@ApiStatus.Internal
public class AuctionConfig {
    /**
     * Gets a double from the config converted to floAuction's proprietary "safe money."
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return "safe money" from config
     */
    public static long getSafeMoneyFromDouble(String path, AuctionScope auctionScope) {
        return Functions.getSafeMoney(getDouble(path, auctionScope));
    }

    /**
     * Gets a double value from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return double from the config
     */
    public static double getDouble(String path, AuctionScope auctionScope) {
        Double result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getDouble(path);
        }

        if (result == null) {
            result = ObsidianAuctions.config.getDouble(path);
        }
        return result;
    }

    /**
     * Gets an integer value from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return integer from the config
     */
    public static int getInt(String path, AuctionScope auctionScope) {
        Integer result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getInteger(path);
        }

        if (result == null) {
            result = ObsidianAuctions.config.getInteger(path);
        }
        return result;
    }

    /**
     * Gets a boolean value from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return boolean from the config
     */
    public static boolean getBoolean(String path, AuctionScope auctionScope) {
        Boolean result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getBoolean(path);
        }

        if (result == null) {
            result = ObsidianAuctions.config.getBoolean(path);
        }
        return result;
    }

    /**
     * Gets a string list from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return string list from the config
     */
    public static List<String> getStringList(String path, AuctionScope auctionScope) {
        List<String> result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getStringList(path);
        }

        if (result == null) {
            result = ObsidianAuctions.config.getStringList(path);
        }
        return result;
    }

    /**
     * Gets a string value from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return string from the config
     */
    public static String getString(String path, AuctionScope auctionScope) {
        String result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getString(path);
        }

        if (result == null) {
            result = ObsidianAuctions.config.getString(path);
        }
        return result;
    }

    /**
     * Gets a uuid value from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return uuid from the config
     */
    public static UUID getUUID(String path, AuctionScope auctionScope) {
        UUID result = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            result = auctionScope.getConfig().getUUID(path);
        }
        if (result == null) {
            result = ObsidianAuctions.config.getUUID(path);
        }
        return result;
    }

    /**
     * Gets a string to string map from the config.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return string to string map from the config
     */
    public static Map<String, String> getStringStringMap(String path, AuctionScope auctionScope) {
        Map<String, String> result = new HashMap<>();

        ConfigurationSection section = null;
        if (auctionScope != null && auctionScope.getConfig() != null && auctionScope.getConfig().hasKey(path)) {
            section = auctionScope.getConfig().getConfigurationSection(path);
        }

        if (section == null) {
            section = ObsidianAuctions.config.getConfigurationSection(path);
        }

        if (section != null) {
            for (String itemCode : section.getKeys()) {
                result.put(itemCode, section.getString(itemCode));
            }
        }
        return result;
    }

    /**
     * Gets a string from the language file.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return string from language file
     */
    public static String getLanguageString(String path, AuctionScope auctionScope) {
        String result = null;
        if (auctionScope != null && auctionScope.getTextConfig() != null && auctionScope.getTextConfig().hasKey(path)) {
            result = auctionScope.getTextConfig().getString(path);
        }

        if (result == null) {
            result = ObsidianAuctions.textConfig.getString(path);
        }
        return result;
    }

    /**
     * Gets a string list from the language file.
     *
     * @param path         the location in the config of the value
     * @param auctionScope the preferred AuctionScope for retrieval
     * @return string list from language file
     */
    public static List<String> getLanguageStringList(String path, AuctionScope auctionScope) {
        List<String> result = null;
        if (auctionScope != null && auctionScope.getTextConfig() != null && auctionScope.getTextConfig().hasKey(path)) {
            result = auctionScope.getTextConfig().getStringList(path);
        }

        if (result == null) {
            result = ObsidianAuctions.textConfig.getStringList(path);
        }
        return result;
    }
} public class ObsidianAuctions extends JavaPlugin {

    private static ObsidianAuctions instance;

    public static ObsidianAuctions get() {
        return instance;
    }

    public static int decimalPlaces = 0;
    public static String decimalRegex = "^[0-9]{0,13}(\\.[0-9]{0,1})?$";
    private File auctionLog = null;
    private boolean suspendAllAuctions = false;
    public static boolean isDamagedAllowed;

    // Config files info.
    public static Configuration config = null;
    public static Configuration textConfig = null;
    private File dataFolder;

    private static int playerScopeCheckTimer;

    private List<AuctionLot> orphanLots = new ArrayList<>();
    private Collection<UUID> voluntarilyDisabledUsers = new HashSet<>();
    private Collection<UUID> suspendedUsers = new HashSet<>();

    private Queue<String> logQueue;

    private MessageManager messageManager;
    private AuctionProhibitionManager prohibitionCache;
    private AuctionLocationManager locationManager;
    private AuctionManager auctionManager;

    //Adventure
    private BukkitAudiences adventure;

    //Cloud
    private CommandManager<CommandSender> commandManager;
    private AnnotationParser<CommandSender> commandParser;

    /*Added values
     *
     */
    public static String guiQueueName;
    public static List<String> itemBlacklist;
    public static boolean itemNameBlackListEnabled;
    public static boolean enableChatMessages;
    public static boolean enableActionbarMessages;
    public static boolean allowRenamedItems;
    public static int actionBarTicks;

    /**
     * Used by AuctionLot to store auction lots which could not be given to players because they were offline.
     *
     * @param auctionLot AuctionLot to save.
     */
    public void saveOrphanLot(AuctionLot auctionLot) {
        this.orphanLots.add(auctionLot);
        FileUtil.save(this.orphanLots, "orphanLots.ser");
    }

    /**
     * Attempts to give lost AuctionLots back to their intended destination.
     *
     * @param player the player to check for missing items
     */
    // Eliminate orphan lots (i.e. try to give the items to a player again).
    public void killOrphan(Player player) {
        if (this.orphanLots != null && this.orphanLots.size() > 0) {
            Iterator<AuctionLot> it = this.orphanLots.iterator();
            while (it.hasNext()) {
                AuctionLot lot = it.next();
                if (lot.getOwner().equalsIgnoreCase(player.getName())) {
                    lot.cancelLot();
                    it.remove();
                }
            }
            FileUtil.save(this.orphanLots, "orphanLots.ser");
        }
    }


    // Vault objects
    private Economy econ = null;
    private Permission perms = null;

    /**
     * Called by Bukkit when initializing.  Sets up basic plugin settings.
     */
    @Override
    public void onEnable() {
        this.logQueue = new ConcurrentLinkedQueue<>();
        this.adventure = BukkitAudiences.create(this);
        instance = this;
        this.dataFolder = this.getDataFolder();
        if (!this.dataFolder.exists()) {
            this.dataFolder.mkdir();
        }

        this.auctionLog = new File(this.dataFolder, "auctions.log");
        if (!this.auctionLog.exists()) {
            try {
                this.auctionLog.createNewFile();
                this.auctionLog.setWritable(true);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        this.saveResource("config.yml", false);
        this.saveResource("language.yml", false);

        File languagesDirectory = new File(this.dataFolder, "item_languages");
        if (!languagesDirectory.exists()) {
            languagesDirectory.mkdirs();
        }

        this.saveResource("item_languages/en-US.yml", false);
        this.loadConfig();

        if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
            logToBukkit("plugin-disabled-no-vault", Level.SEVERE);
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        if (!this.setupVault()) {
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        Class<? extends Placeholder> placeholderClazz = NoPlaceholderImpl.class;
        if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
            placeholderClazz = PapiPlaceholderImpl.class;
        }

        String language = config.getString("language");
        Injector injector = this.inject(config, language, placeholderClazz);
        this.registerListeners(injector);

        BukkitScheduler scheduler = this.getServer().getScheduler();
        scheduler.scheduleSyncRepeatingTask(this, () -> this.auctionManager.checkAuctionQueue(), 20L, 20L);

        long playerScopeCheckInterval = config.getLong("auctionscope-change-check-interval");
        if (playerScopeCheckTimer > 0) scheduler.cancelTask(playerScopeCheckTimer);

        if (playerScopeCheckInterval > 0) {
            playerScopeCheckTimer = scheduler.scheduleSyncRepeatingTask(this, () -> {
                this.auctionManager.sendFarewellMessages();
                this.auctionManager.sendWelcomeMessages();
            }, playerScopeCheckInterval, playerScopeCheckInterval);
        }

        this.orphanLots = FileUtil.load("orphanLots.ser", new ArrayList<>());
        this.voluntarilyDisabledUsers = FileUtil.load("voluntarilyDisabledUsers.ser", new HashSet<>());
        this.suspendedUsers = FileUtil.load("suspendedUsers.ser", new HashSet<>());

        this.commandManager = this.createCommandManager(injector);
        this.commandParser = new AnnotationParser(this.commandManager,
                CommandSender.class, parameters ->
                SimpleCommandMeta.empty());
        this.commandParser.parse(injector.getInstance(AuctionCommands.class));

        this.messageManager.sendPlayerMessage("plugin-enabled", null, (AuctionScope) null);
        scheduler.runTaskTimerAsynchronously(this, this::writeCurrentLog, 20, 20);
    }

    private boolean setupVault() {
        if (!this.setupEconomy()) {
            logToBukkit("plugin-disabled-no-economy", Level.SEVERE);
            return false;
        } else {
            decimalPlaces = Math.max(econ.fractionalDigits(), 0);
            config.set("decimal-places", decimalPlaces);
            if (decimalPlaces < 1) {
                decimalRegex = "^[0-9]{1,13}$";
            } else if (decimalPlaces == 1) {
                decimalRegex = "^[0-9]{0,13}(\\.[0-9])?$";
            } else {
                decimalRegex = "^[0-9]{0,13}(\\.[0-9]{1," + decimalPlaces + "})?$";
            }
        }
        return this.setupPermissions();
    }

    private Injector inject(Configuration config, String itemLanguage, Class<? extends Placeholder> placeholderClazz) {
        File itemLanguagesFolder = new File(this.dataFolder, "item_languages");
        File itemConfig = new File(itemLanguagesFolder, itemLanguage + ".yml");
        Configuration i18nItemConfig = Configuration.load(itemConfig);

        AuctionModule module = new AuctionModule(this, this.adventure, config, i18nItemConfig, placeholderClazz);
        Injector injector = Guice.createInjector(module);
        this.messageManager = injector.getInstance(MessageManager.class);
        this.prohibitionCache = injector.getInstance(AuctionProhibitionManager.class);
        this.locationManager = injector.getInstance(AuctionLocationManager.class);
        this.auctionManager = injector.getInstance(AuctionManager.class);
        return injector;
    }

    private void registerListeners(Injector injector) {
        InjectUtil.collect(Listener.class, injector).forEach(listener -> {
            this.getServer().getPluginManager().registerEvents(listener, this);
        });
    }

    /**
     * Loads config.yml and language.yml configuration files.
     */
    public void loadConfig() {
        File configFile = new File(this.dataFolder, "config.yml");

        File textConfigFile = new File(this.dataFolder, "language.yml");


        config = Configuration.load(configFile);
        textConfig = Configuration.load(textConfigFile);

        //TODO - copy defaults
        /*
        InputStream defConfigStream = plugin.getResource("config.yml");
        InputStream defTextConfigStream = plugin.getResource("language.yml");
         Configuration defConfig = null;
        Configuration defTextConfig = null;
        // Look for defaults in the jar
        if(defConfigStream != null) {
            defConfig = Configuration.load(defConfigStream, ConfigurationType.YAML);
            try {
                defConfigStream.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
        if(defConfig != null) {
            config.setDefaults(defConfig);
        }

        textConfig = null;

        // Look for defaults in the jar
        if(defTextConfigStream != null) {
            InputStreamReader reader = new InputStreamReader(defTextConfigStream);
            defTextConfig = Configuration.load(reader);
            try {
                reader.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
            defTextConfigStream = null;
        }
        if(defTextConfig != null) {
            textConfig.setDefaults(defTextConfig);
        }

        // Clean up the configuration of any unused values.
        Configuration cleanConfig = new Configuration();
        Map<String, Object> configValues = config.getDefaults().getValues();
        for(Map.Entry<String, Object> configEntry : configValues.entrySet()) {
            cleanConfig.set(configEntry.getKey(), config.get(configEntry.getKey()));
        }

        config = cleanConfig;
        config.save();


        Configuration cleanTextConfig = new Configuration();
        Map<String, Object> textConfigValues = textConfig.getDefaults().getValues();
        for(Map.Entry<String, Object> textConfigEntry : textConfigValues.entrySet()) {
            cleanTextConfig.set(textConfigEntry.getKey(), textConfig.get(textConfigEntry.getKey()));
        }
        textConfig = cleanTextConfig;

        textConfig.save();*/

        //Gui queue inventory name
        ObsidianAuctions.guiQueueName = ChatColor.translateAlternateColorCodes('&', config.getString("queue-gui-name"));
        ObsidianAuctions.itemBlacklist = config.getStringList("name-blacklist");
        ObsidianAuctions.itemNameBlackListEnabled = config.getBoolean("name-blacklist-enabled");
        ObsidianAuctions.enableChatMessages = config.getBoolean("enable-chat-messages");
        ObsidianAuctions.enableActionbarMessages = config.getBoolean("enable-actionbar-messages");
        ObsidianAuctions.allowRenamedItems = config.getBoolean("allow-renamed-items");
        ObsidianAuctions.actionBarTicks = config.get("action-bar-ticks", Integer.class, 60);

        //Setup additional floAuction values
        ObsidianAuctions.isDamagedAllowed = config.getBoolean("allow-damaged-items");

        //make values null at the end
        //defConfig = null;
        configFile = null;
        //defTextConfig = null;
        textConfigFile = null;
    }

    /**
     * Called by Bukkit when disabling.  Cancels all auctions and clears data.
     */
    @Override
    public void onDisable() {
        this.writeCurrentLog();
        this.auctionManager.cancelAllAuctions();
        instance = null;
        this.logToBukkit("plugin-disabled", Level.INFO);
        this.auctionLog = null;
        if (this.adventure != null) {
            this.adventure.close();
            this.adventure = null;
        }
    }

    /**
     * Log data to the floAuction log file if logging is enabled.
     *
     * @param playerName   who is initiating the logged event
     * @param message      message to save
     * @param auctionScope the auction scope being referenced if any
     */
    public void log(String playerName, String message, AuctionScope auctionScope) {
        if (AuctionConfig.getBoolean("log-auctions", auctionScope)) {
            String scopeId = "NOSCOPE";
            if (auctionScope != null) {
                scopeId = auctionScope.getScopeId();
            }
            String dateStr = (new Date()).toString();
            String strippedMessage = ChatColor.stripColor(message);
            String log = dateStr + " (" + playerName + ", " + scopeId + "): " + strippedMessage;
            this.logQueue.add(log);
        }
    }

    private void writeCurrentLog() {
        try (BufferedWriter out = new BufferedWriter(new FileWriter(this.auctionLog, true))) {
            String polled;
            while ((polled = this.logQueue.poll()) != null) {
                out.write(polled);
                out.newLine();
            }
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Setup Vault economy.
     *
     * @return success level
     */
    private boolean setupEconomy() {
        try {
            Class.forName("net.milkbowl.vault.economy.Economy");
            RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
            if (rsp == null) {
                return false;
            }
            this.econ = rsp.getProvider();
            return this.econ != null;
        } catch (ClassNotFoundException ex) {
            return false;
        }
    }

    /**
     * Setup Vault permission.
     *
     * @return success level
     */
    private boolean setupPermissions() {
        try {
            Class.forName("net.milkbowl.vault.permission.Permission");
            RegisteredServiceProvider<net.milkbowl.vault.permission.Permission> rsp = Bukkit
                    .getServicesManager()
                    .getRegistration(net.milkbowl.vault.permission.Permission.class);
            this.perms = rsp.getProvider();
            return this.perms != null;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    /**
     * Gets the active auction instance from the scope where the player is.
     *
     * @param player player in reference
     * @return auction instance
     */
    public Auction getPlayerAuction(Player player) {
        if (player == null) {
            return null;
        }
        AuctionScope auctionScope = this.auctionManager.getPlayerScope(player);
        if (auctionScope == null) {
            return null;
        }
        return auctionScope.getActiveAuction();
    }

    public boolean isVoluntarilyDisabled(UUID uuid) {
        return this.voluntarilyDisabledUsers.contains(uuid);
    }

    public boolean addVoluntarilyDisabled(UUID uuid) {
        return this.voluntarilyDisabledUsers.add(uuid);
    }

    public boolean removeVoluntarilyDisabled(UUID uuid) {
        return this.voluntarilyDisabledUsers.remove(uuid);
    }

    public void saveVoluntarilyDisabled() {
        FileUtil.save(this.voluntarilyDisabledUsers, "voluntarilyDisabledUsers.ser");
    }

    public boolean isSuspendedUser(UUID uuid) {
        return this.suspendedUsers.contains(uuid);
    }

    public boolean addSuspendedUser(UUID uuid) {
        return this.suspendedUsers.add(uuid);
    }

    public boolean removeSuspendedUser(UUID uuid) {
        return this.suspendedUsers.remove(uuid);
    }

    public void saveSuspendedUsers() {
        FileUtil.save(this.suspendedUsers, "suspendedUsers.ser");
    }

    /**
     * Prepares chat, prepending prefix and removing colors.
     *
     * @param message      message to prepare
     * @param auctionScope the scope of the destination
     * @return prepared message
     */
    private static String chatPrepClean(String message, AuctionScope auctionScope) {
        message = AuctionConfig.getLanguageString("chat-prefix", auctionScope) + message;
        message = ChatColor.translateAlternateColorCodes('&', message);
        message = ChatColor.stripColor(message);
        return message;
    }

    public MessageManager getMessageManager() {
        return this.messageManager;
    }

    public AuctionProhibitionManager getProhibitionManager() {
        return this.prohibitionCache;
    }

    public Economy getEconomy() {
        return this.econ;
    }

    public Permission getPermission() {
        return this.perms;
    }

    public AuctionLocationManager getAuctionLocationManager() {
        return this.locationManager;
    }

    public AuctionManager getAuctionManager() {
        return this.auctionManager;
    }

    private void logToBukkit(String key, Level level) {
        List<String> messageList = AuctionConfig.getLanguageStringList(key, null);

        String originalMessage;
        if (messageList == null || messageList.size() == 0) {
            originalMessage = AuctionConfig.getLanguageString(key, null);

            if (originalMessage != null && originalMessage.length() != 0) {
                messageList = Arrays.asList(originalMessage.split("(\r?\n|\r)"));
            }
        }
        for (Iterator<String> i = messageList.iterator(); i.hasNext(); ) {
            String messageListItem = i.next();
            this.getLogger().log(level, chatPrepClean(messageListItem, null));
        }
    }

    public void setSuspendAllAuctions(boolean suspend) {
        this.suspendAllAuctions = suspend;
    }

    public boolean getSuspendAllAuctions() {
        return this.suspendAllAuctions;
    }

    private CommandManager<CommandSender> createCommandManager(Injector injector) {
        try {
            PaperCommandManager<CommandSender> commandManager = new PaperCommandManager(this,
                    CommandExecutionCoordinator.simpleCoordinator(),
                    Function.identity(),
                    Function.identity());
            CommandPermissionHandler handler = injector.getInstance(CommandPermissionHandler.class);
            commandManager.registerExceptionHandler(NoPermissionException.class, handler);
            if (commandManager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) {
                commandManager.registerBrigadier();
            }
            return commandManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
} public class Auction {

    private final ObsidianAuctions plugin;
    private final String[] args;
    private UUID ownerUUID;
    private String ownerName;
    private final AuctionScope scope;

    private double extractedPreTax = 0;
    private double extractedPostTax = 0;

    private long startingBid = 0;
    private long minBidIncrement = 0;
    private long buyNow = 0;
    private int quantity = 0;
    private int time = 0;
    private boolean active = false;

    private AuctionLot lot;
    private AuctionBid currentBid = null;
    private final List<AuctionBid> sealedBids = new ArrayList<>();

    private boolean sealed;

    private long nextTickTime = 0;

    // Scheduled timers:
    private int countdown = 0;
    private int countdownTimer = 0;

    //added
    private final ItemStack guiItem;
    private final MessageManager messageManager;

    /**
     * Gets the AuctionScope which hosts this auction.
     *
     * @return the hosting AuctionScope
     */
    public AuctionScope getScope() {
        return this.scope;
    }

    /**
     * Instantiates an auction instance.
     *
     * @param plugin       the active floAuction plugin instance
     * @param auctionOwner the player who is starting the auction
     * @param inputArgs    the command parameters entered in chat
     * @param scope        the hosting AuctionScope
     * @param sealed       whether it is a sealed auction
     */
    public Auction(ObsidianAuctions plugin, Player auctionOwner, String[] inputArgs, AuctionScope scope, boolean sealed, MessageManager messageManager, ItemStack guiItem) {
        this.ownerUUID = auctionOwner.getUniqueId();
        this.ownerName = auctionOwner.getName();
        this.args = Functions.mergeInputArgs(auctionOwner.getUniqueId(), inputArgs, false);
        this.plugin = plugin;
        this.scope = scope;
        this.sealed = sealed;
        this.messageManager = messageManager;
        this.guiItem = guiItem;

        //Override gui item item meta
        if (this.guiItem.hasItemMeta()) {
            if (this.guiItem.getItemMeta().hasLore()) {
                List<String> lore = this.guiItem.getItemMeta().getLore();
                lore.add(ChatColor.BLUE + "Auction by: " + this.getOwnerDisplayName());
                //lore.add(ChatColor.BLUE + "Starting price: " + this.getStartingBid());
                //lore.add(ChatColor.BLUE + "Buy it now price: " + this.buyNow);
                ItemMeta itemMeta = this.guiItem.getItemMeta();
                itemMeta.setLore(lore);
                this.guiItem.setItemMeta(itemMeta);
            } else {
                List<String> lore = new ArrayList<>();
                lore.add(ChatColor.BLUE + "Auction by: " + this.getOwnerDisplayName());
                //lore.add(ChatColor.BLUE + "Starting price: " + this.getStartingBid());
                //lore.add(ChatColor.BLUE + "Buy it now price: " + this.buyNow);
                ItemMeta itemMeta = this.guiItem.getItemMeta();
                itemMeta.setLore(lore);
                this.guiItem.setItemMeta(itemMeta);
            }
        } else {
            List<String> lore = new ArrayList<>();
            lore.add(ChatColor.BLUE + "Auction by: " + this.getOwnerDisplayName());
            //lore.add(ChatColor.BLUE + "Starting price: " + this.getStartingBid());
            //lore.add(ChatColor.BLUE + "Buy it now price: " + this.buyNow);
            ItemMeta itemMeta = this.guiItem.getItemMeta();
            itemMeta.setLore(lore);
            this.guiItem.setItemMeta(itemMeta);
        }

    }

    /**
     * Attempts to start this auction instance.  Returns success.
     *
     * @return whether or not the auction start succeeded
     */
    public boolean start() {
        Player owner = this.plugin.getServer().getPlayer(this.ownerUUID);

        if (ObsidianAuctions.get().getAuctionLocationManager().isInArena(owner)) {
            this.messageManager.sendPlayerMessage("auction-fail-arena", this.ownerUUID, this);
            return false;
        }

        ItemStack typeStack = this.lot.getTypeStack();
        double preAuctionTax = AuctionConfig.getDouble("auction-start-tax", this.scope);

        // Check banned items:
        List<String> bannedItems = AuctionConfig.getStringList("banned-items", this.scope);
        for (String bannedItem : bannedItems) {
            if (Items.isSameItem(typeStack, bannedItem)) {
                this.messageManager.sendPlayerMessage("auction-fail-banned", this.ownerUUID, this);
                return false;
            }
        }

        Map<String, String> taxedItems = AuctionConfig.getStringStringMap("taxed-items", this.scope);
        for (Map.Entry<String, String> entry : taxedItems.entrySet()) {
            if (Items.isSameItem(typeStack, entry.getKey())) {
                String itemTax = entry.getValue();

                if (itemTax.endsWith("a")) {
                    try {
                        preAuctionTax = Double.parseDouble(itemTax.substring(0, itemTax.length() - 1));
                    } catch (Exception e) {
                        // Clearly this isn't a valid number, just forget about it.
                        preAuctionTax = AuctionConfig.getDouble("auction-start-tax", this.scope);
                    }
                } else if (!itemTax.endsWith("%")) {
                    try {
                        preAuctionTax = Double.parseDouble(itemTax);
                        preAuctionTax *= this.quantity;
                    } catch (Exception e) {
                        // Clearly this isn't a valid number, just forget about it.
                        preAuctionTax = AuctionConfig.getDouble("auction-start-tax", this.scope);
                    }
                }
                break;
            }
        }

        if (preAuctionTax > 0D) {
            if (!Functions.hasBalance(this.ownerUUID, preAuctionTax)) {
                this.messageManager.sendPlayerMessage("auction-fail-start-tax", this.ownerUUID, this);
                return false;
            }
        }

        if (!this.lot.addItems(this.quantity, true)) {
            this.messageManager.sendPlayerMessage("auction-fail-insufficient-supply", this.ownerUUID, this);
            return false;
        }

        if (preAuctionTax > 0D) {
            if (Functions.hasBalance(this.ownerUUID, preAuctionTax)) {
                Functions.withdrawPlayer(this.ownerUUID, preAuctionTax);
                this.extractedPreTax = preAuctionTax;
                this.messageManager.sendPlayerMessage("auction-start-tax", this.ownerUUID, this);
                UUID taxDestinationUser = AuctionConfig.getUUID("deposit-tax-to-user", scope);
                if (taxDestinationUser != null) {
                    Functions.depositPlayer(taxDestinationUser, preAuctionTax);
                }
            }
        }

        if (this.buyNow < getStartingBid()) {
            this.buyNow = 0;
        }

        // Check to see if any other plugins have a reason...or they can forever hold their piece.
        AuctionStartEvent auctionStartEvent = new AuctionStartEvent(owner, this);
        this.plugin.getServer().getPluginManager().callEvent(auctionStartEvent);

        if (auctionStartEvent.isCancelled()) {
            this.messageManager.sendPlayerMessage("auction-fail-blocked-by-other-plugin", this.ownerUUID, this);
        } else {
            this.active = true;
            this.messageManager.broadcastAuctionMessage("auction-start", this);

            // Set timer:
            final Auction thisAuction = this;
            this.countdown = this.time;

            this.countdownTimer = this.plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
                if (thisAuction.nextTickTime > System.currentTimeMillis()) {
                    return;
                }
                thisAuction.nextTickTime = thisAuction.nextTickTime + 1000;

                thisAuction.countdown--;
                if (thisAuction.countdown <= 0) {
                    thisAuction.end();
                    return;
                }
                if (!AuctionConfig.getBoolean("suppress-countdown", scope)) {
                    if (thisAuction.countdown < 4) {
                        messageManager.broadcastAuctionMessage("timer-countdown-notification", thisAuction);
                        return;
                    }
                    if (thisAuction.time >= 20) {
                        if (thisAuction.countdown == (thisAuction.time / 2)) {
                            messageManager.broadcastAuctionMessage("timer-countdown-notification", thisAuction);
                        }
                    }
                }
            }, 1L, 1L);
            this.nextTickTime = System.currentTimeMillis() + 1000;

            info(null, true);
        }

        return this.active;
    }

    /**
     * Sends auction info to chat.
     *
     * @param sender        the CommandSender initiating the request
     * @param fullBroadcast whether to send the message to everyone in the hosting AuctionScope
     */
    public void info(CommandSender sender, boolean fullBroadcast) {
        List<String> messageKeys = new ArrayList<>();
        UUID playerUUID = null;
        if (sender instanceof Player) {
            playerUUID = ((Player) sender).getUniqueId();
        }
        if (!this.active) {
            if (sender instanceof Player) {
                this.messageManager.sendPlayerMessage("auction-info-no-auction", playerUUID, this);
            }
            return;
        } else {
            messageKeys.add("auction-info");
        }
        if (fullBroadcast) {
            this.messageManager.broadcastAuctionMessage(messageKeys, this);
        } else {
            this.messageManager.sendPlayerMessage(messageKeys, playerUUID, this);
        }
    }

    /**
     * Cancels the Auction instance and disposes of it normally.
     */
    public void cancel() {
        this.plugin.getServer().getPluginManager().callEvent(new AuctionEndEvent(this, true));
        this.messageManager.broadcastAuctionMessage("auction-cancel", this);

        if (this.lot != null) {
            this.lot.cancelLot();
        }
        if (this.currentBid != null) {
            this.currentBid.cancelBid();
        }
        this.dispose();
    }

    /**
     * Cancels the Auction instance redirecting all goods to an approved authority.
     * If the authority is not approved, Auction instance will not be cancelled.
     *
     * @param authority the name of a player authorized to confiscate auctions
     */
    public void confiscate(Player authority) {
        this.plugin.getServer().getPluginManager().callEvent(new AuctionEndEvent(this, true));
        this.ownerName = authority.getName();
        this.ownerUUID = authority.getUniqueId();
        this.messageManager.broadcastAuctionMessage("confiscate-success", this);
        if (this.lot != null) {
            this.lot.winLot(authority.getUniqueId(), authority.getName());
        }
        if (this.currentBid != null) {
            this.currentBid.cancelBid();
        }
        this.dispose();
    }

    /**
     * Ends an auction normally sending money and goods to their earned destinations.
     */
    public void end() {
        AuctionEndEvent auctionEndEvent = new AuctionEndEvent(this, false);
        this.plugin.getServer().getPluginManager().callEvent(auctionEndEvent);
        if (auctionEndEvent.isCancelled()) {
            this.messageManager.broadcastAuctionMessage("auction-cancel", this);
            if (this.lot != null) {
                this.lot.cancelLot();
            }
            if (this.currentBid != null) {
                this.currentBid.cancelBid();
            }
        } else {
            if (this.currentBid == null || this.lot == null) {
                this.messageManager.broadcastAuctionMessage("auction-end-nobids", this);
                if (this.lot != null) {
                    this.lot.cancelLot();
                }
                if (this.currentBid != null) {
                    this.currentBid.cancelBid();
                }
            } else {
                this.messageManager.broadcastAuctionMessage("auction-end", this);
                this.lot.winLot(currentBid.getBidderUUID(), currentBid.getBidderName());
                this.currentBid.winBid();
            }
        }
        this.dispose();
    }

    /**
     * Disposes of the remains of a terminated auction, purging the timer, refunding sealed bid losers and removing self from host scope.
     */
    private void dispose() {
        this.plugin.getServer().getScheduler().cancelTask(this.countdownTimer);
        this.sealed = false;
        for (AuctionBid sealedBid : this.sealedBids) {
            sealedBid.cancelBid();
        }
        this.scope.setActiveAuction(null);
    }

    /**
     * Checks all auction parameters and environment factors to determine if the Auction instance can legitimately start.
     *
     * @return whether the auction can begin
     */
    public boolean isValid() {
        if (!isValidOwner()) {
            return false;
        } else if (!parseHeldItem()) {
            return false;
        } else if (!parseArgs()) {
            return false;
        } else if (!isValidAmount()) {
            return false;
        } else if (!isValidStartingBid()) {
            return false;
        } else if (!isValidIncrement()) {
            return false;
        } else if (!isValidTime()) {
            return false;
        }
        return isValidBuyNow();
    }

    /**
     * Parses a bid command.
     *
     * @param bidder    Player attempting to bid
     * @param inputArgs parameters entered in chat
     */
    public void bid(Player bidder, String[] inputArgs) {
        if (bidder == null) {
            return;
        }
        UUID playerUUID = bidder.getUniqueId();

        if (ObsidianAuctions.get().getAuctionLocationManager().isInArena(bidder)) {
            this.messageManager.sendPlayerMessage("bid-fail-arena", playerUUID, this);
            return;
        }

        // BuyNow
        if (AuctionConfig.getBoolean("allow-buynow", scope) && inputArgs.length > 0) {
            if (inputArgs[0].equalsIgnoreCase("buy")) {

                if (this.buyNow == 0 || (this.currentBid != null && currentBid.getBidAmount() >= this.buyNow)) {
                    this.messageManager.sendPlayerMessage("bid-fail-buynow-expired", playerUUID, this);
                } else {
                    inputArgs[0] = Double.toString(Functions.getUnsafeMoney(this.buyNow));
                    if (inputArgs[0].endsWith(".0")) {
                        inputArgs[0] = inputArgs[0].substring(0, inputArgs[0].length() - 2);
                    }
                    AuctionBid bid = new AuctionBid(this, bidder, inputArgs);
                    if (bid.getError() != null) {
                        failBid(bid, bid.getError());
                        return;
                    } else {
                        // raisOwnBid does nothing if it's not the current bidder.
                        if (this.currentBid != null) {
                            bid.raiseOwnBid(this.currentBid);
                        }

                        // Let other plugins figure out any reasons why this buy shouldn't happen.
                        AuctionBidEvent auctionBidEvent = new AuctionBidEvent(bidder, this, Functions.getUnsafeMoney(bid.getBidAmount()), Functions.getUnsafeMoney(bid.getMaxBidAmount()), true);
                        this.plugin.getServer().getPluginManager().callEvent(auctionBidEvent);
                        if (auctionBidEvent.isCancelled()) {
                            this.failBid(bid, "bid-fail-blocked-by-other-plugin");
                        } else {
                            this.setNewBid(bid, null);
                            this.end();
                        }
                    }
                }
                return;
            }
        }

        // Normal bid
        AuctionBid bid = new AuctionBid(this, bidder, inputArgs);
        if (bid.getError() != null) {
            this.failBid(bid, bid.getError());
            return;
        }

        if (this.currentBid == null) {
            if (bid.getBidAmount() < getStartingBid()) {
                this.failBid(bid, "bid-fail-under-starting-bid");
                return;
            }
            // Let other plugins figure out any reasons why this buy shouldn't happen.
            AuctionBidEvent auctionBidEvent = new AuctionBidEvent(bidder, this, Functions.getUnsafeMoney(bid.getBidAmount()), Functions.getUnsafeMoney(bid.getMaxBidAmount()), true);
            this.plugin.getServer().getPluginManager().callEvent(auctionBidEvent);
            if (auctionBidEvent.isCancelled()) {
                failBid(bid, "bid-fail-blocked-by-other-plugin");
            } else {
                setNewBid(bid, "bid-success-no-challenger");
            }
            return;
        }

        long previousBidAmount = this.currentBid.getBidAmount();
        long previousMaxBidAmount = this.currentBid.getMaxBidAmount();
        if (this.currentBid.getBidderName().equals(bidder.getName())) {
            if (bid.raiseOwnBid(this.currentBid)) {
                // Let other plugins figure out any reasons why this buy shouldn't happen.
                AuctionBidEvent auctionBidEvent = new AuctionBidEvent(bidder, this, Functions.getUnsafeMoney(bid.getBidAmount()), Functions.getUnsafeMoney(bid.getMaxBidAmount()), true);
                this.plugin.getServer().getPluginManager().callEvent(auctionBidEvent);
                if (auctionBidEvent.isCancelled()) {
                    this.failBid(bid, "bid-fail-blocked-by-other-plugin");
                } else {
                    this.setNewBid(bid, "bid-success-update-own-bid");
                }
            } else {
                if (previousMaxBidAmount < currentBid.getMaxBidAmount()) {
                    this.failBid(bid, "bid-success-update-own-maxbid");
                } else {
                    this.failBid(bid, "bid-fail-already-current-bidder");
                }
            }
            return;
        }

        AuctionBid winner;
        AuctionBid loser;

        if (AuctionConfig.getBoolean("use-old-bid-logic", this.scope)) {
            if (bid.getMaxBidAmount() > this.currentBid.getMaxBidAmount()) {
                winner = bid;
                loser = this.currentBid;
            } else {
                winner = this.currentBid;
                loser = bid;
            }
            winner.raiseBid(Math.max(winner.getBidAmount(), Math.min(winner.getMaxBidAmount(), loser.getBidAmount() + this.minBidIncrement)));
        } else {
            // If you follow what this does, congratulations.
            long baseBid = Math.max(bid.getBidAmount(), this.currentBid.getBidAmount() + this.minBidIncrement);

            int prevSteps = (int) Math.floor((double) (this.currentBid.getMaxBidAmount() - baseBid + this.minBidIncrement) / this.minBidIncrement / 2);
            int newSteps = (int) Math.floor((double) (bid.getMaxBidAmount() - baseBid) / this.minBidIncrement / 2);

            if (newSteps >= prevSteps) {
                winner = bid;
                winner.raiseBid(baseBid + (Math.max(0, prevSteps) * this.minBidIncrement * 2));
                loser = this.currentBid;
            } else {
                winner = this.currentBid;
                winner.raiseBid(baseBid + (Math.max(0, newSteps + 1) * this.minBidIncrement * 2) - this.minBidIncrement);
                loser = bid;
            }
        }

        if (previousBidAmount <= winner.getBidAmount()) {
            // Did the new bid win?
            if (winner.equals(bid)) {
                // Let other plugins figure out any reasons why this buy shouldn't happen.
                AuctionBidEvent auctionBidEvent = new AuctionBidEvent(bidder, this, Functions.getUnsafeMoney(bid.getBidAmount()), Functions.getUnsafeMoney(bid.getMaxBidAmount()), true);
                this.plugin.getServer().getPluginManager().callEvent(auctionBidEvent);
                if (auctionBidEvent.isCancelled()) {
                    failBid(bid, "bid-fail-blocked-by-other-plugin");
                } else {
                    setNewBid(bid, "bid-success-outbid");
                }
            } else {
                // Did the old bid have to raise the bid to stay winner?
                if (previousBidAmount < winner.getBidAmount()) {
                    if (!this.sealed && !AuctionConfig.getBoolean("broadcast-bid-updates", scope)) {
                        this.messageManager.broadcastAuctionMessage("bid-auto-outbid", this);
                    }
                    failBid(bid, "bid-fail-auto-outbid");
                } else {
                    if (!this.sealed) {
                        this.messageManager.sendPlayerMessage("bid-fail-too-low", bid.getBidderUUID(), this);
                    }
                    this.failBid(bid, null);
                }
            }
        } else {
            // Seriously don't know what could cause this, but might as well take care of it.
            this.messageManager.sendPlayerMessage("bid-fail-too-low", bid.getBidderUUID(), this);
        }
    }

    /**
     * Disposes of a failed bid attempt.
     *
     * @param attemptedBid the attempted bid
     * @param reason       message key to send to looser
     */
    private void failBid(AuctionBid attemptedBid, String reason) {
        attemptedBid.cancelBid();
        if (this.sealed && (attemptedBid.getError() == null || attemptedBid.getError().isEmpty())) {
            this.messageManager.sendPlayerMessage("bid-success-sealed", attemptedBid.getBidderUUID(), this);
        } else {
            this.messageManager.sendPlayerMessage(reason, attemptedBid.getBidderUUID(), this);
        }
    }

    /**
     * Assigns new bid and alerts those in the hosting AuctionScope.
     *
     * @param newBid the new bid
     * @param reason message key to broadcast
     */
    private void setNewBid(AuctionBid newBid, String reason) {
        AuctionBid prevBid = this.currentBid;

        if (AuctionConfig.getBoolean("expire-buynow-at-first-bid", this.scope)) {
            this.buyNow = 0;
        }

        if (this.currentBid != null) {
            this.currentBid.cancelBid();
        }
        this.currentBid = newBid;
        if (this.sealed) {
            this.messageManager.sendPlayerMessage("bid-success-sealed", newBid.getBidderUUID(), this);
        } else if (AuctionConfig.getBoolean("broadcast-bid-updates", this.scope)) {
            this.messageManager.broadcastAuctionMessage(reason, this);
        } else {
            this.messageManager.sendPlayerMessage(reason, newBid.getBidderUUID(), this);
            if (prevBid != null && newBid.getBidderName().equalsIgnoreCase(prevBid.getBidderName())) {
                this.messageManager.sendPlayerMessage(reason, prevBid.getBidderUUID(), this);
            }
        }
        ObsidianAuctions.get().getAuctionManager().addParticipant(newBid.getBidderUUID(), this.scope);
        if (this.currentBid.getBidAmount() >= this.buyNow) {
            this.buyNow = 0;
        }

        // see if antisnipe is enabled...
        if (!this.sealed && AuctionConfig.getBoolean("anti-snipe", this.scope) && this.getRemainingTime() <= AuctionConfig.getInt("anti-snipe-prevention-seconds", this.scope)) {
            this.addToRemainingTime(AuctionConfig.getInt("anti-snipe-prevention-seconds", this.scope));
            this.messageManager.broadcastAuctionMessage("anti-snipe-time-added", this);
        }
    }

    /**
     * Checks the item in hand to see if it's valid and allowed.
     *
     * @return acceptability of held item for auctioning
     */
    private boolean parseHeldItem() {
        Player owner = this.plugin.getServer().getPlayer(this.ownerUUID);
        if (this.lot != null) {
            return true;
        }
        ItemStack heldItem = LegacyUtil.getItemInMainHand(owner);
        if (heldItem == null || heldItem.getAmount() == 0) {
            this.messageManager.sendPlayerMessage("auction-fail-hand-is-empty", this.ownerUUID, this);
            return false;
        }
        this.lot = new AuctionLot(heldItem, this.ownerUUID, this.ownerName);

        ItemStack itemType = this.lot.getTypeStack();

        if (!AuctionConfig.getBoolean("allow-damaged-items", scope) && itemType.getType().getMaxDurability() > 0 && LegacyUtil.getDurability(itemType) > 0) {
            this.messageManager.sendPlayerMessage("auction-fail-damaged-item", this.ownerUUID, this);
            this.lot = null;
            return false;
        }

        String displayName = Items.getDisplayName(itemType);
        if (displayName == null) {
            displayName = "";
        }
        //Implementing allowing auctioned mob-spawners
        //if display name is not empty do function if not fall through to next if
        if (!displayName.isEmpty()) {
            if (ObsidianAuctions.itemNameBlackListEnabled) {
                String lowerCaseDisplay = displayName.toLowerCase();
                for (String string : ObsidianAuctions.itemBlacklist) {
                    if (lowerCaseDisplay.contains(string)) {
                        this.messageManager.sendPlayerMessage("auction-fail-blacklist-name", this.ownerUUID, this);
                        return false;
                    }
                }
            }
        }

        String typeStr = itemType.getType().toString();
        if ((typeStr.equals("MOB_SPAWNER") || typeStr.equals("SPAWNER")) && !AuctionConfig.getBoolean("allow-mobspawners", scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-spawner", this.ownerUUID, this);
            this.lot = null;
            return false;
        }

        if (!displayName.isEmpty() && !AuctionConfig.getBoolean("allow-renamed-items", scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-renamed-item", this.ownerUUID, this);
            this.lot = null;
            return false;
        }

        // Check lore:
        String[] lore = Items.getLore(heldItem);
        List<String> bannedLore = AuctionConfig.getStringList("banned-lore", scope);
        if (lore != null && bannedLore != null) {
            for (String s : bannedLore) {
                for (String value : lore) {
                    if (value.toLowerCase().contains(s.toLowerCase())) {
                        this.messageManager.sendPlayerMessage("auction-fail-banned-lore", this.ownerUUID, this);
                        this.lot = null;
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Parses arguments entered into chat.
     *
     * @return acceptability of entered arguments
     */
    private boolean parseArgs() {
        // (amount) (starting price) (increment) (time) (buynow)
        if (!parseArgAmount()) {
            return false;
        } else if (!parseArgStartingBid()) {
            return false;
        } else if (!parseArgIncrement()) {
            return false;
        } else if (!parseArgTime()) {
            return false;
        } else return parseArgBuyNow();
    }

    /**
     * Checks auction starter ability to start auction.
     *
     * @return acceptability of starter auctioning
     */
    private boolean isValidOwner() {
        if (this.ownerName == null) {
            this.messageManager.sendPlayerMessage("auction-fail-invalid-owner", null, this);
            return false;
        }
        return true;
    }

    /**
     * Checks lot quantity range and availability in starter's inventory.
     *
     * @return acceptability of lot quantity
     */
    private boolean isValidAmount() {
        if (this.quantity <= 0) {
            this.messageManager.sendPlayerMessage("auction-fail-quantity-too-low", this.ownerUUID, this);
            return false;
        }

        // TODO: Add config setting for max quantity.

        if (!Items.hasAmount(this.ownerName, this.quantity, this.lot.getTypeStack())) {
            this.messageManager.sendPlayerMessage("auction-fail-insufficient-supply", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Checks starting bid.
     *
     * @return if starting bid is ok
     */
    private boolean isValidStartingBid() {
        if (this.startingBid < 0) {
            this.messageManager.sendPlayerMessage("auction-fail-starting-bid-too-low", this.ownerUUID, this);
            return false;
        } else if (this.startingBid > AuctionConfig.getSafeMoneyFromDouble("max-starting-bid", scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-starting-bid-too-high", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Checks minimum bid increment.
     *
     * @return if minimum bid increment is okay
     */
    private boolean isValidIncrement() {
        if (getMinBidIncrement() < AuctionConfig.getSafeMoneyFromDouble("min-bid-increment", this.scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-increment-too-low", this.ownerUUID, this);
            return false;
        }
        if (getMinBidIncrement() > AuctionConfig.getSafeMoneyFromDouble("max-bid-increment", scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-increment-too-high", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Checks BuyNow amount.
     *
     * @return if BuyNow amount is okay
     */
    private boolean isValidBuyNow() {
        if (getBuyNow() < 0) {
            this.messageManager.sendPlayerMessage("auction-fail-buynow-too-low", this.ownerUUID, this);
            return false;
        } else if (getBuyNow() > AuctionConfig.getSafeMoneyFromDouble("max-buynow", scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-buynow-too-high", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Checks auction time limit.
     *
     * @return if auction time limit is okiedokie
     */
    private boolean isValidTime() {
        if (this.time < AuctionConfig.getInt("min-auction-time", this.scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-time-too-low", this.ownerUUID, this);
            return false;
        } else if (this.time > AuctionConfig.getInt("max-auction-time", this.scope)) {
            this.messageManager.sendPlayerMessage("auction-fail-time-too-high", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Parses quantity argument.
     *
     * @return if quantity argument parsed correctly
     */
    private boolean parseArgAmount() {
        if (this.quantity > 0) {
            return true;
        }

        ItemStack lotType = this.lot.getTypeStack();
        if (this.args.length > 0) {
            if (this.args[0].equalsIgnoreCase("this") || this.args[0].equalsIgnoreCase("hand")) {
                this.quantity = lotType.getAmount();
            } else if (this.args[0].equalsIgnoreCase("all")) {
                this.quantity = Items.getAmount(this.ownerName, lotType);
            } else if (args[0].matches("[0-9]{1,7}")) {
                this.quantity = Integer.parseInt(this.args[0]);
            } else {
                this.messageManager.sendPlayerMessage("parse-error-invalid-quantity", this.ownerUUID, this);
                return false;
            }
        } else {
            this.quantity = lotType.getAmount();
        }
        if (this.quantity < 0) {
            this.messageManager.sendPlayerMessage("parse-error-invalid-quantity", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Parses starting bid argument.
     *
     * @return if argument parsed correctly
     */
    private boolean parseArgStartingBid() {
        if (this.startingBid > 0) {
            return true;
        }

        if (this.args.length > 1) {
            if (this.args[1].isEmpty()) {
                this.messageManager.sendPlayerMessage("parse-error-invalid-starting-bid", this.ownerUUID, this);
                return false;
            } else if (!args[1].matches(ObsidianAuctions.decimalRegex)) {
                this.messageManager.sendPlayerMessage("parse-error-invalid-starting-bid", this.ownerUUID, this);
                return false;
            }
            this.startingBid = Functions.getSafeMoney(Double.parseDouble(args[1]));
        } else {
            this.startingBid = AuctionConfig.getSafeMoneyFromDouble("default-starting-bid", this.scope);
        }
        if (this.startingBid < 0) {
            this.messageManager.sendPlayerMessage("parse-error-invalid-starting-bid", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Parses minimum bid increment.
     *
     * @return if minimum bid increment parsed correctly
     */
    private boolean parseArgIncrement() {
        if (this.minBidIncrement > 0) {
            return true;
        }

        if (this.args.length > 2) {
            if (!this.args[2].isEmpty() && args[2].matches(ObsidianAuctions.decimalRegex)) {
                this.minBidIncrement = Functions.getSafeMoney(Double.parseDouble(this.args[2]));
            } else {
                this.messageManager.sendPlayerMessage("parse-error-invalid-bid-increment", this.ownerUUID, this);
                return false;
            }
        } else {
            this.minBidIncrement = AuctionConfig.getSafeMoneyFromDouble("default-bid-increment", this.scope);
        }
        if (this.minBidIncrement < 0) {
            this.messageManager.sendPlayerMessage("parse-error-invalid-bid-increment", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Parses time argument.
     *
     * @return if time argument parsed correctly
     */
    private boolean parseArgTime() {
        if (this.time > 0) {
            return true;
        }

        if (this.args.length > 3) {
            if (this.args[3].matches("[0-9]{1,7}")) {
                this.time = Integer.parseInt(this.args[3]);
            } else {
                this.messageManager.sendPlayerMessage("parse-error-invalid-time", this.ownerUUID, this);
                return false;
            }
        } else {
            this.time = AuctionConfig.getInt("default-auction-time", this.scope);
        }
        if (this.time < 0) {
            this.messageManager.sendPlayerMessage("parse-error-invalid-time", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Parses BuyNow argument.
     *
     * @return if BuyNow argument parsed correctly
     */
    private boolean parseArgBuyNow() {

        if (this.sealed || !AuctionConfig.getBoolean("allow-buynow", this.scope)) {
            this.buyNow = 0;
            return true;
        }

        if (getBuyNow() > 0) {
            return true;
        }

        if (this.args.length > 4) {
            if (!this.args[4].isEmpty() && this.args[4].matches(ObsidianAuctions.decimalRegex)) {
                this.buyNow = Functions.getSafeMoney(Double.parseDouble(args[4]));
            } else {
                this.messageManager.sendPlayerMessage("parse-error-invalid-buynow", this.ownerUUID, this);
                return false;
            }
        } else {
            this.buyNow = 0;
        }
        if (getBuyNow() < 0) {
            this.messageManager.sendPlayerMessage("parse-error-invalid-buynow", this.ownerUUID, this);
            return false;
        }
        return true;
    }

    /**
     * Gets the specified minimum bid increment for this Auction instance.
     *
     * @return minimum bid increment
     */
    public long getMinBidIncrement() {
        return this.minBidIncrement;
    }

    /**
     * Gets a type stack of the items being auctioned.
     *
     * @return stack of example items
     */
    public ItemStack getLotType() {
        if (this.lot == null) {
            return null;
        }
        return this.lot.getTypeStack();
    }

    /**
     * Gets quantity of the auctioned lot.
     *
     * @return amount being auctioned
     */
    public int getLotQuantity() {
        if (this.lot == null) {
            return 0;
        }
        return this.lot.getQuantity();
    }

    /**
     * Gets the lowest amount first bid can be.
     *
     * @return lowest possible starting bid in floAuction's proprietary "safe money"
     */
    public long getStartingBid() {
        long effectiveStartingBid = this.startingBid;
        if (effectiveStartingBid == 0) {
            effectiveStartingBid = this.minBidIncrement;
        }
        return effectiveStartingBid;
    }

    /**
     * Gets the AuctionBid object for the current winning bid.
     *
     * @return AuctionBid object of leader
     */
    public AuctionBid getCurrentBid() {
        return this.currentBid;
    }

    /**
     * Gets the UUID of the player who started and therefore "owns" the auction.
     *
     * @return auction owner uuid
     */
    public UUID getOwnerUUID() {
        return this.ownerUUID;
    }

    /**
     * Gets the name of the player who started and therefore "owns" the auction.
     *
     * @return auction owner name
     */
    public String getOwnerName() {
        return this.ownerName;
    }

    /**
     * Gets the amount of time remaining in the auction.
     *
     * @return number of seconds remaining in auction
     */
    public int getRemainingTime() {
        return this.countdown;
    }

    /**
     * Gets the originally specified auction time limit.  This does not take into account time added by anti-snipe being triggered.
     *
     * @return original auction length in seconds
     */
    public int getTotalTime() {
        return this.time;
    }

    /**
     * Adds to the remaining auction countdown.
     *
     * @param secondsToAdd
     * @return
     */
    public int addToRemainingTime(int secondsToAdd) {
        this.countdown += secondsToAdd;
        return this.countdown;
    }

    /**
     * Gets the amount specified for BuyNow.
     *
     * @return BuyNow amount in floAuction's proprietary "safe money"
     */
    public long getBuyNow() {
        return this.buyNow;
    }

    public String getOwnerDisplayName() {
        Player ownerPlayer = this.plugin.getServer().getPlayer(this.ownerUUID);
        if (ownerPlayer != null) {
            return ownerPlayer.getDisplayName();
        } else {
            return this.ownerName;
        }
    }

    public ItemStack getGuiItem() {
        return this.guiItem;
    }

    public boolean isSealed() {
        return this.sealed;
    }

    public MessageManager getMessageManager() {
        return this.messageManager;
    }

    public List<AuctionBid> getSealedBids() {
        return this.sealedBids;
    }

    public double getExtractedPreTax() {
        return this.extractedPreTax;
    }

    public double getExtractedPostTax() {
        return this.extractedPostTax;
    }

    public void setExtractedPostTax(double taxes) {
        this.extractedPostTax = taxes;
    }
} public class AuctionBid {

    private final Auction auction;
    private final UUID bidderUUID;
    private final String bidderName;
    private long bidAmount = 0;
    private long maxBidAmount = 0;
    private String error;
    private final String[] args;
    private double reserve = 0;

    /**
     * Constructor that validates bidder, parses arguments and reserves maximum bid funds.
     *
     * @param auction   the auction being bid upon
     * @param player    the player doing the bidding
     * @param inputArgs the parameters entered in chat
     */
    public AuctionBid(Auction auction, Player player, String[] inputArgs) {
        this.auction = auction;
        this.bidderUUID = player.getUniqueId();
        this.bidderName = player.getName();
        this.args = inputArgs;
        if (!validateBidder()) return;
        if (!parseArgs()) return;
        if (!reserveBidFunds()) return;
    }

    /**
     * Removes funds from bidder's account and stores them in the AuctionBid instance.
     *
     * @return true if funds are reserved, false if an issue was encountered
     */
    private boolean reserveBidFunds() {
        long amountToReserve = 0;
        long previousSealedReserve = 0;
        AuctionBid currentBid = this.auction.getCurrentBid();

        for (int i = 0; i < this.auction.getSealedBids().size(); i++) {
            if (this.auction.getSealedBids().get(i).getBidderName().equalsIgnoreCase(this.getBidderName())) {
                previousSealedReserve += this.auction.getSealedBids().get(i).getBidAmount();
                this.auction.getSealedBids().remove(i);
                i--;
            }
        }

        if (currentBid != null && currentBid.getBidderName().equalsIgnoreCase(bidderName)) {
            // Same bidder: only reserve difference.
            if (this.maxBidAmount > currentBid.getMaxBidAmount() + previousSealedReserve) {
                amountToReserve = this.maxBidAmount - currentBid.getMaxBidAmount() - previousSealedReserve;
            } else {
                // Nothing needing reservation.
                return true;
            }
        } else {
            amountToReserve = this.maxBidAmount;
        }
        if (Functions.withdrawPlayer(this.bidderUUID, amountToReserve)) {
            this.reserve = Functions.getUnsafeMoney(amountToReserve);
            return true;
        } else {
            this.error = "bid-fail-cant-allocate-funds";
            return false;
        }
    }

    /**
     * Refunds reserve for unsealed auctions or queues reserve refund for sealed auctions.
     */
    public void cancelBid() {
        if (this.auction.isSealed()) {
            // Queue reserve refund.
            this.auction.getSealedBids().add(this);
            ObsidianAuctions.get().getAuctionManager().addParticipant(this.getBidderUUID(), this.auction.getScope());
        } else {
            // Refund reserve.
            Functions.depositPlayer(this.bidderUUID, this.reserve);
            this.reserve = 0;
        }

    }

    /**
     * Process winning bid, gives winnings to auction owner, returns the remainder of the reserve and appropriates end of auction taxes.
     */
    public void winBid() {
        double unsafeBidAmount = Functions.getUnsafeMoney(this.bidAmount);

        // Extract taxes:
        double taxes = 0D;
        double taxPercent = AuctionConfig.getDouble("auction-end-tax-percent", this.auction.getScope());
        ItemStack typeStack = this.auction.getLotType();

        // TODO: Check this line for possible NULL
        for (Map.Entry<String, String> entry : AuctionConfig.getStringStringMap("taxed-items", this.auction.getScope()).entrySet()) {
            if (Items.isSameItem(typeStack, entry.getKey())) {
                if (entry.getValue().endsWith("%")) {
                    try {
                        taxPercent = Double.valueOf(entry.getValue().substring(0, entry.getValue().length() - 1));
                    } catch (Exception e) {
						/* Clearly this isn't a valid number, just forget about it.
						   taxPercent = AuctionConfig.getDouble("auction-end-tax-percent", auction.getScope());
						   On second thought, this is already the value, so just keep it.
						 */
                    }
                }
                break;
            }
        }


        if (taxPercent > 0D) {
            taxes = unsafeBidAmount * (taxPercent / 100D);
            this.auction.setExtractedPostTax(taxes);
            this.auction.getMessageManager().sendPlayerMessage("auction-end-tax", this.auction.getOwnerUUID(), this.auction);
            unsafeBidAmount -= taxes;
            UUID taxDestinationUser = AuctionConfig.getUUID("deposit-tax-to-user", this.auction.getScope());
            if (taxDestinationUser != null) {
                Functions.depositPlayer(taxDestinationUser, taxes);
            }
        }

        // Apply winnings to auction owner.
        Functions.depositPlayer(this.auction.getOwnerUUID(), unsafeBidAmount);

        // Refund remaining reserve.
        Functions.depositPlayer(this.bidderUUID, this.reserve - unsafeBidAmount - taxes);

        this.reserve = 0;
    }

    /**
     * Checks existence, permission, scope and prohibitions on player trying to bid.
     *
     * @return whether player can bid
     */
    private boolean validateBidder() {
        if (this.bidderName == null) {
            this.error = "bid-fail-no-bidder";
            return false;
        } else if (ObsidianAuctions.get().getProhibitionManager().isOnProhibition(this.bidderUUID, false)) {
            this.error = "remote-plugin-prohibition-reminder";
            return false;
        } else if (!ObsidianAuctions.get().getAuctionLocationManager().checkLocation(this.bidderUUID)) {
            this.error = "bid-fail-outside-auctionhouse";
            return false;
        } else if (bidderUUID.equals(auction.getOwnerUUID()) && !AuctionConfig.getBoolean("allow-bid-on-own-auction", this.auction.getScope())) {
            this.error = "bid-fail-is-auction-owner";
            return false;
        }
        return true;
    }

    /**
     * Parses bid arguments.
     *
     * @return whether args are acceptable
     */
    private boolean parseArgs() {
        if (!parseArgBid()) {
            return false;
        } else return parseArgMaxBid();
    }

    /**
     * Prepares two bids from the same player to compete against each other.
     *
     * @param otherBid the previous bid
     * @return whether it's the same player bidding
     */
    public boolean raiseOwnBid(AuctionBid otherBid) {
        if (this.bidderName.equalsIgnoreCase(otherBid.bidderName)) {
            // Move reserve money here.
            this.reserve = this.reserve + otherBid.reserve;
            otherBid.reserve = 0;

            // Maxbid only updates up.
            this.maxBidAmount = Math.max(this.maxBidAmount, otherBid.maxBidAmount);
            otherBid.maxBidAmount = this.maxBidAmount;

            if (this.bidAmount > otherBid.bidAmount) {
                // The bid has been raised.
                return true;
            } else {
                // Put the reserve on the other bid because we're cancelling this one.
                otherBid.reserve = this.reserve;
                this.reserve = 0;
                return false;
            }
        } else {
            // Don't take reserve unless it's the same person's money.
            return false;
        }
    }

    /**
     * Attempt to raise this bid.
     *
     * @param newBidAmount
     * @return success of raising bid
     */
    public boolean raiseBid(Long newBidAmount) {
        if (newBidAmount <= this.maxBidAmount && newBidAmount >= this.bidAmount) {
            this.bidAmount = newBidAmount;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Parses main bid argument.
     *
     * @return acceptability of bid argument
     */
    private boolean parseArgBid() {
        if (this.args.length > 0) {
            if (!this.args[0].isEmpty() && args[0].matches(ObsidianAuctions.decimalRegex)) {
                this.bidAmount = Functions.getSafeMoney(Double.parseDouble(this.args[0]));
                /*Should fix the bug that allowed over-sized payments
                 */
                if (!Functions.hasBalance(this.bidderUUID, this.bidAmount)) {
                    this.error = "bid-fail-cant-allocate-funds";
                    return false;
                } else if (this.bidAmount == 0) {
                    this.error = "parse-error-invalid-bid";
                    return false;
                }
            } else {
                this.error = "parse-error-invalid-bid";
                return false;
            }
        } else {
            if (this.auction.isSealed() || !AuctionConfig.getBoolean("allow-auto-bid", this.auction.getScope())) {
                this.error = "bid-fail-bid-required";
                return false;
            } else {
                // Leaving it up to automatic:
                this.bidAmount = 0;
            }
        }
        // If the person bids 0, make it automatically the next increment (unless it's the current bidder).
        if (this.bidAmount == 0) {
            AuctionBid currentBid = this.auction.getCurrentBid();
            if (currentBid == null) {
                // Use the starting bid if no one has bid yet.
                this.bidAmount = auction.getStartingBid();
                if (this.bidAmount == 0) {
                    // Unless the starting bid is 0, then use the minimum bid increment.
                    this.bidAmount = auction.getMinBidIncrement();
                }
            } else if (currentBid.getBidderName().equalsIgnoreCase(this.bidderName)) {
                // We are the current bidder, so use previous.  Don't auto-up our own bid.
                this.bidAmount = currentBid.bidAmount;
            } else {
                this.bidAmount = currentBid.getBidAmount() + this.auction.getMinBidIncrement();
            }
        }
        if (this.bidAmount <= 0) {
            this.error = "parse-error-invalid-bid";
            return false;
        }
        return true;
    }

    /**
     * Parse max bid argument.
     *
     * @return acceptability of max bid
     */
    private boolean parseArgMaxBid() {
        if (!AuctionConfig.getBoolean("allow-max-bids", this.auction.getScope()) || this.auction.isSealed()) {
            // Just ignore it.
            this.maxBidAmount = this.bidAmount;
            return true;
        }
        if (this.args.length > 1) {
            if (!args[1].isEmpty() && args[1].matches(ObsidianAuctions.decimalRegex)) {
                this.maxBidAmount = Functions.getSafeMoney(Double.parseDouble(this.args[1]));
            } else {
                this.error = "parse-error-invalid-max-bid";
                return false;
            }
        }
        this.maxBidAmount = Math.max(this.bidAmount, this.maxBidAmount);
        if (this.maxBidAmount <= 0) {
            this.error = "parse-error-invalid-max-bid";
            return false;
        }
        return true;
    }

    /**
     * Gets the error which may have occurred.
     *
     * @return the error
     */
    public String getError() {
        return this.error;
    }

    /**
     * Gets the uuid of the bidder.
     *
     * @return uuid of bidder
     */
    public UUID getBidderUUID() {
        return this.bidderUUID;
    }

    /**
     * Gets the name of the bidder.
     *
     * @return name of bidder
     */
    public String getBidderName() {
        return this.bidderName;
    }

    public String getBidderDisplayName() {
        Player bidderPlayer = Bukkit.getPlayer(this.bidderName);
        if (bidderPlayer != null) {
            return bidderPlayer.getDisplayName();
        } else {
            return this.bidderName;
        }
    }

    /**
     * Gets the amount currently bid in floAuction's proprietary "safe money."
     *
     * @return
     */
    public long getBidAmount() {
        return this.bidAmount;
    }

    /**
     * Gets the amount maximum bid for this instance in floAuction's proprietary "safe money."
     *
     * @return
     */
    public long getMaxBidAmount() {
        return this.maxBidAmount;
    }
} public class AuctionScope {

    private Auction activeAuction = null;
    private final String scopeId;
    private final String name;
    private final String type;
    private final List<Auction> auctionQueue = new ArrayList<>();
    private long lastAuctionDestroyTime = 0;

    // Definitions
    private List<String> worlds = null;
    private Location minHouseLocation = null;
    private Location maxHouseLocation = null;
    private boolean locationChecked = false;

    private final ConfigurationSection config;
    private final ConfigurationSection textConfig;

    /**
     * Constructor to make new scopes from the name, config and language config files.
     *
     * @param scopeId    name of scope
     * @param config     configuration
     * @param textConfig language config
     */
    public AuctionScope(String scopeId, ConfigurationSection config, ConfigurationSection textConfig) {
        this.scopeId = scopeId;
        this.name = this.resolveName(config);
        this.type = config.getString("type");
        this.config = config;
        this.textConfig = textConfig;
    }

    /**
     * Checks whether the scopes definition is valid.
     *
     * @return true if valid location, false if invalid
     */
    private boolean scopeLocationIsValid() {
        if (this.locationChecked) {
            return worlds != null || this.minHouseLocation != null || this.maxHouseLocation != null;
        } else if (this.type.equalsIgnoreCase("worlds")) {
            this.worlds = config.getStringList("worlds");
        } else if (type.equalsIgnoreCase("house")) {
            String world = config.getString("house-world");
            if (world == null || world.isEmpty()) {
                this.minHouseLocation = null;
                this.maxHouseLocation = null;
            } else {
                double configMinX = this.config.getDouble("house-min-x");
                double configMinY = this.config.getDouble("house-min-y");
                double configMinZ = this.config.getDouble("house-min-z");
                double configMaxX = this.config.getDouble("house-max-x");
                double configMaxY = this.config.getDouble("house-max-y");
                double configMaxZ = this.config.getDouble("house-max-z");
                double minX = Math.min(configMinX, configMaxX);
                double minY = Math.min(configMinY, configMaxY);
                double minZ = Math.min(configMinZ, configMaxZ);
                double maxX = Math.max(configMinX, configMaxX);
                double maxY = Math.max(configMinY, configMaxY);
                double maxZ = Math.max(configMinZ, configMaxZ);
                this.minHouseLocation = new Location(Bukkit.getWorld(world), minX, minY, minZ);
                this.maxHouseLocation = new Location(Bukkit.getWorld(world), maxX, maxY, maxZ);
            }
        }
        this.locationChecked = true;
        return this.worlds != null || this.minHouseLocation != null || this.maxHouseLocation != null;
    }

    private String resolveName(ConfigurationSection config) {
        String name = config.getString("name");
        return name != null ? name : this.scopeId;
    }

    /**
     * Retrieves and instance of the active auction from this scope.
     *
     * @return active auction instance
     */
    public Auction getActiveAuction() {
        return this.activeAuction;
    }

    /**
     * Gets the number of queued auctions.
     *
     * @return length of auction queue
     */
    public int getAuctionQueueLength() {
        return this.auctionQueue.size();
    }

    /**
     * Sets the passed in auction instance as the active auction.
     *
     * @param auction auction instance to set as active
     */
    public void setActiveAuction(Auction auction) {
        if (this.activeAuction != null && auction == null) {
            this.lastAuctionDestroyTime = System.currentTimeMillis();
            ObsidianAuctions.get().getAuctionManager().checkAuctionQueue();
        }
        this.activeAuction = auction;
    }

    /**
     * Adds an auction instance to the auction queue for this scope.
     *
     * @param auctionToQueue auction instance to queue
     */
    public void queueAuction(Auction auctionToQueue) {
        UUID playerUUID = auctionToQueue.getOwnerUUID();
        MessageManager messageManager = auctionToQueue.getMessageManager();

        if (this.activeAuction == null) {
            // Queuing because of interval not yet timed out.
            // Allow a queue of 1 to override if 0 for this condition.
            if (Math.max(AuctionConfig.getInt("max-auction-queue-length", this), 1) <= this.auctionQueue.size()) {
                messageManager.sendPlayerMessage("auction-queue-fail-full", playerUUID, auctionToQueue);
                return;
            }
        } else {
            if (AuctionConfig.getInt("max-auction-queue-length", this) <= 0) {
                messageManager.sendPlayerMessage("auction-fail-auction-exists", playerUUID, auctionToQueue);
                return;
            } else if (this.activeAuction.getOwnerUUID().equals(playerUUID)) {
                messageManager.sendPlayerMessage("auction-queue-fail-current-auction", playerUUID, auctionToQueue);
                return;
            } else if (AuctionConfig.getInt("max-auction-queue-length", this) <= auctionQueue.size()) {
                messageManager.sendPlayerMessage("auction-queue-fail-full", playerUUID, auctionToQueue);
                return;
            }
        }
        for (int i = 0; i < this.auctionQueue.size(); i++) {
            if (this.auctionQueue.get(i) != null) {
                Auction queuedAuction = this.auctionQueue.get(i);
                if (queuedAuction.getOwnerUUID().equals(playerUUID)) {
                    messageManager.sendPlayerMessage("auction-queue-fail-in-queue", playerUUID, auctionToQueue);
                    return;
                }
            }
        }
        if ((this.auctionQueue.size() == 0 && System.currentTimeMillis() - this.lastAuctionDestroyTime >= AuctionConfig.getInt("min-auction-interval-secs", this) * 1000) || auctionToQueue.isValid()) {
            this.auctionQueue.add(auctionToQueue);
            ObsidianAuctions.get().getAuctionManager().addParticipant(playerUUID, this);
            ObsidianAuctions.get().getAuctionManager().checkAuctionQueue();
            if (this.auctionQueue.contains(auctionToQueue)) {
                messageManager.sendPlayerMessage("auction-queue-enter", playerUUID, auctionToQueue);
            }
        }
    }

    /**
     * Checks the auction queue to see if the next auction is ready to start.
     */
    void checkThisAuctionQueue() {
        if (this.activeAuction != null) {
            return;
        } else if (System.currentTimeMillis() - this.lastAuctionDestroyTime < AuctionConfig.getInt("min-auction-interval-secs", this) * 1000) {
            return;
        } else if (this.auctionQueue.size() == 0) {
            return;
        }
        Auction auction = this.auctionQueue.remove(0);
        if (auction == null) {
            return;
        }
        MessageManager messageManager = auction.getMessageManager();

        UUID playerUUID = auction.getOwnerUUID();
        Player player = Bukkit.getPlayer(playerUUID);
        if (player == null || !player.isOnline()) {
            return;
        } else if (ObsidianAuctions.get().getProhibitionManager().isOnProhibition(auction.getOwnerUUID(), false)) {
            messageManager.sendPlayerMessage("remote-plugin-prohibition-reminder", playerUUID, auction);
            return;
        } else if (!AuctionConfig.getBoolean("allow-gamemode-creative", this) && player.getGameMode() == GameMode.CREATIVE) {
            messageManager.sendPlayerMessage("auction-fail-gamemode-creative", playerUUID, auction);
            return;
        } else if (!ObsidianAuctions.get().getPermission().has(player, "auction.start")) {
            messageManager.sendPlayerMessage("auction-fail-permissions", playerUUID, auction);
            return;
        } else if (!auction.isValid()) {
            return;
        }
        this.activeAuction = auction;
        if (!auction.start()) {
            this.activeAuction = null;
        }
    }

    /**
     * Checks to see if the player is inside the scope boundaries.
     *
     * @param player player to check
     * @return whether he's in the scope
     */
    public boolean isPlayerInScope(Player player) {
        if (player == null) {
            return false;
        }
        return this.isLocationInScope(player.getLocation());
    }

    /**
     * Checks to see if a location is inside the scope boundaries.
     *
     * @param location location to check
     * @return whether it's in the scope
     */
    public boolean isLocationInScope(Location location) {
        if (location == null) {
            return false;
        }
        World world = location.getWorld();
        if (world == null) {
            return false;
        }
        String worldName = world.getName();
        if (!scopeLocationIsValid()) {
            return false;
        }
        if (this.type.equalsIgnoreCase("worlds")) {
            for (int i = 0; i < this.worlds.size(); i++) {
                if (this.worlds.get(i).equalsIgnoreCase(worldName) || this.worlds.get(i).equalsIgnoreCase("*")) {
                    return true;
                }
            }
        } else if (this.type.equalsIgnoreCase("house")) {
            if (this.minHouseLocation == null || this.maxHouseLocation == null) {
                return false;
            } else if (!location.getWorld().equals(this.minHouseLocation.getWorld())) {
                return false;
            } else if (location.getX() > Math.max(this.minHouseLocation.getX(), this.maxHouseLocation.getX()) || location.getX() < Math.min(minHouseLocation.getX(), this.maxHouseLocation.getX())) {
                return false;
            } else if (location.getZ() > Math.max(this.minHouseLocation.getZ(), this.maxHouseLocation.getZ()) || location.getZ() < Math.min(minHouseLocation.getZ(), this.maxHouseLocation.getZ())) {
                return false;
            } else
                return !(location.getY() > Math.max(this.minHouseLocation.getY(), this.maxHouseLocation.getY())) && !(location.getY() < Math.min(this.minHouseLocation.getY(), this.maxHouseLocation.getY()));
        }
        return false;
    }

    /**
     * Retrieves the configuration for this scope.
     *
     * @return config for the scope
     */
    @ApiStatus.Internal
    public ConfigurationSection getConfig() {
        return this.config.getConfigurationSection("config");
    }

    /**
     * Gets the name of the scope.
     *
     * @return name of the scope
     */
    public String getName() {
        return this.name;
    }

    /**
     * Gets the id of the scope.
     *
     * @return id of the scope
     */
    public String getScopeId() {
        return this.scopeId;
    }

    /**
     * Retrieves the language configuration for this scope.
     *
     * @return language config for the scope
     */
    @ApiStatus.Internal
    public ConfigurationSection getTextConfig() {
        return this.textConfig;
    }

    /**
     * Retrieves the list of auctions queued.
     *
     * @return auction queue
     */
    public List<Auction> getAuctionQueue() {
        return this.auctionQueue;
    }

    /**
     * Gets the position of the named player's auction in the queue or zero if not in queue.
     *
     * @param playerUUID uuid of player
     * @return players position in queue or zero if not in queue
     */
    public int getQueuePosition(UUID playerUUID) {
        for (int i = 0; i < this.auctionQueue.size(); i++) {
            Auction auction = this.auctionQueue.get(i);
            if (auction.getOwnerUUID().equals(playerUUID)) {
                return i + 1;
            }
        }
        return 0;
    }

    @ApiStatus.Internal
    public void clearAuctionQueue() {
        this.auctionQueue.clear();
    }

    public boolean cancelActiveAuction() {
        if (this.activeAuction != null) {
            this.activeAuction.cancel();
            return true;
        }
        return false;
    }
} public interface TranslationFactory {

    String getTranslation(ItemStack itemStack);
} @ApiStatus.Internal
public interface Placeholder {

    String replace(Player player, String message);

} @ApiStatus.Internal
@ApiStatus.NonExtendable
public final class Functions {

    public static String formatTime(int seconds, AuctionScope auctionScope) {
        String returnTime;
        if (seconds >= 60) {
            returnTime = AuctionConfig.getLanguageString("time-format-minsec", auctionScope);
            returnTime = returnTime.replace("%s", Integer.toString(seconds % 60));
            returnTime = returnTime.replace("%m", Integer.toString((seconds - (seconds % 60)) / 60));
        } else {
            returnTime = AuctionConfig.getLanguageString("time-format-seconly", auctionScope);
            returnTime = returnTime.replace("%s", Integer.toString(seconds));
        }
        return returnTime;
    }

    public static String removeUselessDecimal(String number) {
        if (number.endsWith(".0")) {
            number = number.replace(".0", "");
        }
        return number;
    }

    // Merges player's preset with the current specifications and system defaults.
    public static String[] mergeInputArgs(UUID playerUUID, String[] inputArgs, boolean validateArgs) {
        // Get existing defaults (if present)
        String[] resultArgs = new String[]{"this",
                removeUselessDecimal(Double.toString(AuctionConfig.getDouble("default-starting-bid", null))),
                removeUselessDecimal(Double.toString(AuctionConfig.getDouble("default-bid-increment", null))),
                Integer.toString(AuctionConfig.getInt("default-auction-time", null)), "0"};

        // Size increased in 2.10.0
        if (resultArgs.length < 5) {
            String[] tmp = resultArgs.clone();
            resultArgs = new String[]{tmp[0], tmp[1], tmp[2], tmp[3], "0"};
        }

        // Remove the "start" and "prep" args:
        String[] processArgs = inputArgs;
        if (processArgs.length > 0) {
            if (processArgs[0].equalsIgnoreCase("start") || processArgs[0].equalsIgnoreCase("s") || processArgs[0].equalsIgnoreCase("prep") || processArgs[0].equalsIgnoreCase("p")) {
                processArgs = new String[inputArgs.length - 1];
                System.arraycopy(inputArgs, 1, processArgs, 0, inputArgs.length - 1);
            }
        }

        // Quantity:
        if (processArgs.length > 0) {
            if (!processArgs[0].equalsIgnoreCase("-")) {
                resultArgs[0] = processArgs[0];
            }
            if (validateArgs) {
                // This is similar to the validation in Auction.java but without verifying availability.
                if (!resultArgs[0].equalsIgnoreCase("this") && !resultArgs[0].equalsIgnoreCase("hand") && !resultArgs[0].equalsIgnoreCase("all") && !resultArgs[0].matches("[0-9]{1,7}")) {
                    ObsidianAuctions.get().getMessageManager().sendPlayerMessage("parse-error-invalid-quantity", playerUUID, (AuctionScope) null);
                    return null;
                }
            }

            // Starting Price:
            if (processArgs.length > 1) {
                if (!processArgs[1].equalsIgnoreCase("-")) {
                    resultArgs[1] = processArgs[1];
                }
                if (validateArgs) {
                    if (resultArgs[1].isEmpty() || !resultArgs[1].matches(ObsidianAuctions.decimalRegex)) {
                        ObsidianAuctions.get().getMessageManager().sendPlayerMessage("parse-error-invalid-starting-bid", playerUUID, (AuctionScope) null);
                        return null;
                    }
                }

                // Increment
                if (processArgs.length > 2) {
                    if (!processArgs[2].equalsIgnoreCase("-")) {
                        resultArgs[2] = processArgs[2];
                    }
                    if (validateArgs) {
                        if (resultArgs[2].isEmpty() || !resultArgs[2].matches(ObsidianAuctions.decimalRegex)) {
                            ObsidianAuctions.get().getMessageManager().sendPlayerMessage("parse-error-invalid-max-bid", playerUUID, (AuctionScope) null);
                            return null;
                        }
                    }

                    // Time
                    if (processArgs.length > 3) {
                        if (!processArgs[3].equalsIgnoreCase("-")) {
                            resultArgs[3] = processArgs[3];
                        }
                        if (validateArgs) {
                            if (!resultArgs[3].matches("[0-9]{1,7}")) {
                                ObsidianAuctions.get().getMessageManager().sendPlayerMessage("parse-error-invalid-time", playerUUID, (AuctionScope) null);
                                return null;
                            }
                        }

                        // BuyNow
                        if (processArgs.length > 4) {
                            if (!processArgs[4].equalsIgnoreCase("-")) {
                                resultArgs[4] = processArgs[4];
                            }
                            if (validateArgs) {
                                if (resultArgs[4].isEmpty() || !resultArgs[4].matches(ObsidianAuctions.decimalRegex)) {
                                    ObsidianAuctions.get().getMessageManager().sendPlayerMessage("parse-error-invalid-buynow", playerUUID, (AuctionScope) null);
                                    return null;
                                }
                            }
                        }
                    }
                }
            }
        }

        return resultArgs;

    }

    // Money functions.
    public static String formatAmount(long safeMoney) {
        return formatAmount(getUnsafeMoney(safeMoney));
    }

    public static String formatAmount(double unsafeMoney) {
        String vaultFormat = ObsidianAuctions.get().getEconomy().format(unsafeMoney);
        return vaultFormat;
    }

    public static boolean hasBalance(UUID uuid, double preAuctionTax) {
        OfflinePlayer player = getOfflinePlayer(uuid);
        if (player == null) {
            return false;
        }
        return ObsidianAuctions.get().getEconomy().has(player, preAuctionTax);
    }

    public static boolean withdrawPlayer(UUID uuid, long safeMoney) {
        return withdrawPlayer(uuid, getUnsafeMoney(safeMoney));
    }

    public static boolean withdrawPlayer(UUID uuid, double unsafeMoney) {
        OfflinePlayer player = getOfflinePlayer(uuid);
        if (player == null) {
            return false;
        }
        EconomyResponse receipt = ObsidianAuctions.get().getEconomy().withdrawPlayer(player, unsafeMoney);
        return receipt.transactionSuccess();
    }

    public static boolean depositPlayer(UUID uuid, double unsafeMoney) {
        OfflinePlayer player = getOfflinePlayer(uuid);
        if (player == null) {
            return false;
        }
        EconomyResponse receipt = ObsidianAuctions.get().getEconomy().depositPlayer(player, unsafeMoney);
        return receipt.transactionSuccess();
    }

    public static long getSafeMoney(Double money) {
        DecimalFormat twoDForm = new DecimalFormat("#");
        return Long.valueOf(twoDForm.format(money * Math.pow(10, ObsidianAuctions.decimalPlaces)));
    }

    public static double getUnsafeMoney(long money) {
        return (double) money / Math.pow(10, ObsidianAuctions.decimalPlaces);
    }

    public static OfflinePlayer getOfflinePlayer(UUID uuid) {
        OfflinePlayer player = Bukkit.getServer().getPlayer(uuid);
        return player != null ? player : Bukkit.getServer().getOfflinePlayer(uuid);
    }

    private Functions() {
    }
} @ApiStatus.Internal
@ApiStatus.NonExtendable
public final class Items {

    private static final Map<Enchantment, String> enchantmentNames = new HashMap<>();
    private static final Map<Integer, String> enchantmentLevels = new HashMap<>();

    static {
        registerEnchantment("PROTECTION_ENVIRONMENTAL", "Protection");
        registerEnchantment("PROTECTION_FIRE", "Fire Protection");
        registerEnchantment("PROTECTION_FALL", "Feather Falling");
        registerEnchantment("PROTECTION_EXPLOSIONS", "Blast Protection");
        registerEnchantment("PROTECTION_PROJECTILE", "Projectile Protection");
        registerEnchantment("OXYGEN", "Respiration");
        registerEnchantment("WATER_WORKER", "Aqua Afinity");
        registerEnchantment("DEPTH_STRIDER", "Depth Strider");
        registerEnchantment("BINDING_CURSE", "Curse of Bindings");
        registerEnchantment("DAMAGE_ALL", "Sharpness");
        registerEnchantment("DAMAGE_UNDEAD", "Smite");
        registerEnchantment("DAMAGE_ARTHROPODS", "Bane of Arthropods");
        registerEnchantment("LOOT_BONUS_MOBS", "Looting");
        registerEnchantment("DIG_SPEED", "Efficiency");
        registerEnchantment("DURABILITY", "Unbreaking");
        registerEnchantment("LOOT_BONUS_BLOCKS", "Fortune");
        registerEnchantment("ARROW_DAMAGE", "Power");
        registerEnchantment("ARROW_KNOCKBACK", "Punch");
        registerEnchantment("ARROW_FIRE", "Flame");
        registerEnchantment("ARROW_INFINITE", "Infinity");
        registerEnchantment("LUCK", "Luck of the Sea");
        registerEnchantment("VANISHING_CURSE", "Curse of Vanishing");

        enchantmentLevels.put(0, "");
        enchantmentLevels.put(1, "I");
        enchantmentLevels.put(2, "II");
        enchantmentLevels.put(3, "III");
        enchantmentLevels.put(4, "IV");
        enchantmentLevels.put(5, "V");
    }

    private static boolean registerEnchantment(String enchantmentName, String alias) {
        Enchantment enchantment = Enchantment.getByName(enchantmentName);
        if (enchantment == null) {
            return false;
        }
        enchantmentNames.put(enchantment, alias);
        return true;
    }

    private static int firstPartial(ItemStack item, ItemStack[] inventory) {
        if (item == null) {
            return -1;
        }
        for (int i = 0; i < inventory.length; i++) {
            ItemStack cItem = inventory[i];
            if (cItem != null && cItem.getAmount() < cItem.getMaxStackSize() && isSameItem(item, cItem)) {
                return i;
            }
        }
        return -1;
    }

    private static int firstEmpty(ItemStack[] inventory) {
        for (int i = 0; i < inventory.length; i++) {
            if (inventory[i] == null) {
                return i;
            }
        }
        return -1;
    }

    // Most of this function was copied from CraftBukkit.  The above functions too.

    public static void saferItemGive(PlayerInventory playerInventory, ItemStack item) {
        // After bukkit is fixed:
        // player.getInventory().addItem(givingItems);

        while (true) {
            // Do we already have a stack of it?
            int firstPartial = firstPartial(item, playerInventory.getStorageContents());

            // Drat! no partial stack
            if (firstPartial == -1) {
                // Find a free spot!
                int firstFree = firstEmpty(playerInventory.getStorageContents());

                if (firstFree == -1) {
                    // No space at all!
                    // Bukkit returns unplaced items, but floAuction only calls this after checking for space.
                    break;
                } else {
                    // Again, floAuction checks for this elsewhere before calling this...technically this would never occur, no reason to code for it.

                    // More than a single stack!
/*                    if (item.getAmount() > getMaxStackSize(item)) {
                        CraftItemStack stack = new CraftItemStack(item.getTypeId(), getMaxStackSize(item), item.getDurability());
                        stack.addUnsafeEnchantments(item.getEnchantments());
                        playerInventory.setItem(firstFree, stack);
                        item.setAmount(item.getAmount() - getMaxStackSize(item));
                    } else {*/
                    // Just store it
                    playerInventory.setItem(firstFree, item);
                    break;
//                    }
                }
            } else {
                // So, apparently it might only partially fit, well lets do just that
                ItemStack partialItem = playerInventory.getItem(firstPartial);

                int amount = item.getAmount();
                int partialAmount = partialItem.getAmount();
                int maxAmount = partialItem.getMaxStackSize();

                // Check if it fully fits
                if (amount + partialAmount <= maxAmount) {
                    partialItem.setAmount(amount + partialAmount);
                    break;
                }

                // It fits partially
                partialItem.setAmount(maxAmount);
                item.setAmount(amount + partialAmount - maxAmount);
            }
        }

    }


    public static String[] getLore(ItemStack item) {
        if (item == null) return null;
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) return null;
        List<String> pageList = itemMeta.getLore();
        if (pageList == null) return null;
        String[] pages = new String[pageList.size()];
        for (int i = 0; i < pageList.size(); i++) {
            pages[i] = pageList.get(i);
        }
        return pages;
    }


    public static void setLore(ItemStack item, String[] pages) {
        if (item == null || pages == null) return;
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) return;
        List<String> pageList = new ArrayList<>();
        Collections.addAll(pageList, pages);
        itemMeta.setLore(pageList);
        item.setItemMeta(itemMeta);
    }


    public static Map<Enchantment, Integer> getStoredEnchantments(ItemStack item) {
        if (item == null) {
            return null;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return null;
        }
        if (itemMeta instanceof EnchantmentStorageMeta) {
            return ((EnchantmentStorageMeta) itemMeta).getStoredEnchants();
        }
        return null;
    }


    public static void addStoredEnchantment(ItemStack item, Enchantment enchantment, Integer level, boolean ignoreLevelRestriction) {
        if (item == null) {
            return;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return;
        }
        if (itemMeta instanceof EnchantmentStorageMeta) {
            EnchantmentStorageMeta storageMeta = (EnchantmentStorageMeta) itemMeta;
            storageMeta.addStoredEnchant(enchantment, level, ignoreLevelRestriction);
            item.setItemMeta(storageMeta);
        }
    }


    public static Integer getFireworkPower(ItemStack item) {
        if (item == null) {
            return null;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return null;
        } else if (itemMeta instanceof FireworkMeta) {
            FireworkMeta fireworkMeta = ((FireworkMeta) itemMeta);
            return fireworkMeta.getPower();
        }
        return null;
    }


    public static void setFireworkPower(ItemStack item, Integer power) {
        if (item == null) {
            return;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return;
        } else if (itemMeta instanceof FireworkMeta) {
            FireworkMeta fireworkMeta = ((FireworkMeta) itemMeta);
            fireworkMeta.setPower(power);
            item.setItemMeta(fireworkMeta);
        }
    }


    public static FireworkEffect[] getFireworkEffects(ItemStack item) {
        if (item == null) {
            return null;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return null;
        } else if (itemMeta instanceof FireworkMeta) {
            List<FireworkEffect> effectList = ((FireworkMeta) itemMeta).getEffects();
            FireworkEffect[] effects = new FireworkEffect[effectList.size()];
            for (int i = 0; i < effectList.size(); i++) {
                effects[i] = effectList.get(i);
            }
            return effects;
        } else if (itemMeta instanceof FireworkEffectMeta) {
            FireworkEffect[] effects = new FireworkEffect[1];
            effects[0] = ((FireworkEffectMeta) itemMeta).getEffect();
            return effects;
        }
        return null;
    }


    public static void setFireworkEffects(ItemStack item, FireworkEffect[] effects) {
        if (item == null || effects == null) {
            return;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return;
        } else if (itemMeta instanceof FireworkMeta) {
            FireworkMeta fireworkMeta = ((FireworkMeta) itemMeta);
            fireworkMeta.addEffects(effects);
            item.setItemMeta(fireworkMeta);
        } else if (itemMeta instanceof FireworkEffectMeta) {
            if (effects.length > 0) {
                FireworkEffectMeta fireworkEffectMeta = ((FireworkEffectMeta) itemMeta);
                fireworkEffectMeta.setEffect(effects[0]);
                item.setItemMeta(fireworkEffectMeta);
            }
        }
    }


    public static Integer getRepairCost(ItemStack item) {
        if (item == null) {
            return null;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return null;
        } else if (itemMeta instanceof Repairable) {
            return ((Repairable) itemMeta).getRepairCost();
        }
        return null;
    }


    public static void setRepairCost(ItemStack item, Integer repairCost) {
        if (item == null) return;
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) return;
        if (itemMeta instanceof Repairable) {
            Repairable repairable = ((Repairable) itemMeta);
            repairable.setRepairCost(repairCost);
            item.setItemMeta((ItemMeta) repairable);
        }
        return;
    }


    public static String getDisplayName(ItemStack item) {
        if (item == null) return null;
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) return null;
        return itemMeta.getDisplayName();
    }


    public static void setDisplayName(ItemStack item, String name) {
        if (item == null) return;
        if (name == null) return;
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) return;
        itemMeta.setDisplayName(name);
        item.setItemMeta(itemMeta);
    }


    public static String getBookAuthor(ItemStack book) {
        if (book == null) return null;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return null;
        if (itemMeta instanceof BookMeta) {
            return ((BookMeta) itemMeta).getAuthor();
        }
        return null;
    }


    public static void setBookAuthor(ItemStack book, String author) {
        if (book == null) return;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return;
        if (itemMeta instanceof BookMeta) {
            BookMeta bookMeta = ((BookMeta) itemMeta);
            bookMeta.setAuthor(author);
            book.setItemMeta(bookMeta);
        }
        return;
    }


    public static String getBookTitle(ItemStack book) {
        if (book == null) return null;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return null;
        if (itemMeta instanceof BookMeta) {
            return ((BookMeta) itemMeta).getTitle();
        }
        return null;
    }


    public static void setBookTitle(ItemStack book, String title) {
        if (book == null) return;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return;
        if (itemMeta instanceof BookMeta) {
            BookMeta bookMeta = ((BookMeta) itemMeta);
            bookMeta.setTitle(title);
            book.setItemMeta(bookMeta);
        }
        return;
    }


    public static String[] getBookPages(ItemStack book) {
        if (book == null) return null;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return null;
        if (itemMeta instanceof BookMeta) {
            List<String> pageList = ((BookMeta) itemMeta).getPages();
            String[] pages = new String[pageList.size()];
            for (int i = 0; i < pageList.size(); i++) {
                pages[i] = pageList.get(i);
            }
            return pages;
        }
        return null;
    }


    public static void setBookPages(ItemStack book, String[] pages) {
        if (book == null || pages == null) return;
        ItemMeta itemMeta = book.getItemMeta();
        if (itemMeta == null) return;
        if (itemMeta instanceof BookMeta) {
            BookMeta bookMeta = ((BookMeta) itemMeta);
            bookMeta.setPages(pages);
            book.setItemMeta(bookMeta);
        }
    }

    // Some of this was taken from Vault's item classes.

    public static boolean isSameItem(ItemStack item, String searchString) {
        Material mat;
        short damageId = 0;
        if (searchString.contains(",")) {
            String[] split = searchString.split(",");
            mat = Material.valueOf(split[0]);
            damageId = Short.parseShort(split[1]);
        } else {
            mat = Material.valueOf(searchString);
        }
        if (damageId != 0) {
            return isSameItem(item, new ItemStack(mat, 1, damageId));
        }
        return isSameItem(item, new ItemStack(mat, 1));
    }


    public static boolean isSameItem(ItemStack item1, ItemStack item2) {
        return item1.isSimilar(item2);
    }


    public static int getMaxStackSize(ItemStack item) {
        if (item == null) {
            return 0;
        }
        int maxStackSize = item.getType().getMaxStackSize();
        return maxStackSize;
    }


    public static int getSpaceForItem(Player player, ItemStack item) {
        int maxstack = getMaxStackSize(item);
        int space = 0;
        ItemStack[] items = player.getInventory().getStorageContents();
        for (ItemStack current : items) {
            if (current == null) {
                space += maxstack;
                continue;
            }
            if (isSameItem(item, current)) {
                space += maxstack - current.getAmount();
            }
        }
        return space;
    }


    public static boolean hasSpace(Player player, int needed, ItemStack item) {
        return getSpaceForItem(player, item) >= needed;
    }


    public static boolean hasAmount(String ownerName, int amount, ItemStack compareItem) {
        int has = getAmount(ownerName, compareItem);
        return has >= amount;
    }


    public static int getAmount(String ownerName, ItemStack compareItem) {
        if (Bukkit.getPlayer(ownerName) == null) return 0;
        PlayerInventory inventory = Bukkit.getPlayer(ownerName).getInventory();
        ItemStack[] items = inventory.getStorageContents();
        int has = 0;
        for (ItemStack item : items) {
            if (isSameItem(compareItem, item)) {
                has += item.getAmount();
            }
        }
        return has;
    }


    public static void remove(String playerName, int amount, ItemStack compareItem) {
        Player player = Bukkit.getPlayer(playerName);
        if (player != null) {
            PlayerInventory inventory = player.getInventory();

            // Remove held item first:
            ItemStack hand = LegacyUtil.getItemInMainHand(player);
            if (isSameItem(compareItem, hand)) {
                int heldAmount = hand.getAmount();
                if (heldAmount <= amount) {
                    amount -= heldAmount;
                    inventory.clear(inventory.getHeldItemSlot());
                } else {
                    hand.setAmount(heldAmount - amount);
                    amount = 0;
                }
            }

            int counter = amount;
            int leftover = 0;

            // Remove from other stacks:
            for (int invIndex = 0; invIndex < inventory.getSize(); invIndex++) {
                ItemStack current = inventory.getItem(invIndex);

                if (current == null || current.getAmount() <= 0)
                    continue;

                if (!isSameItem(compareItem, current))
                    continue;

                if (current.getAmount() > counter) {
                    leftover = current.getAmount() - counter;
                }

                if (leftover != 0) {
                    current.setAmount(leftover);
                    counter = 0;
                    break;
                } else {
                    counter -= current.getAmount();
                    inventory.clear(invIndex);
                }
            }
        }
    }

    public static boolean isEnchantable(ItemStack heldItem) {
        ItemStack item = new ItemStack(heldItem.getType());
        for (Enchantment ench : Enchantment.values()) {
            if (ench.canEnchantItem(item)) {
                return true;
            }
        }
        return false;
    }


    public static String getEnchantmentName(Entry<Enchantment, Integer> enchantment) {
        Enchantment enchantmentType = enchantment.getKey();
        int enchantmentLevel = enchantment.getValue();
        String enchantmentName;
        if (enchantmentNames.get(enchantmentType) != null) {
            enchantmentName = enchantmentNames.get(enchantmentType) + " ";
        } else {
            enchantmentName = EnumUtil.formatName(enchantmentType.getName()) + " ";
        }
        if (enchantmentLevels.get(enchantmentLevel) != null) {
            enchantmentName += enchantmentLevels.get(enchantmentLevel) + " ";
        } else {
            enchantmentName += enchantmentLevel + " ";
        }
        return enchantmentName;
    }


    public static Boolean isPlayerHead(ItemStack lot) {
        if (lot == null) {
            return false;
        } else if (lot.getItemMeta() == null) {
            return false;
        }
        String lotType = lot.getType().name();
        return lotType.equals("PLAYER_HEAD") || (lotType.equals("SKULL_ITEM") && LegacyUtil.getDurability(lot) == 3);
    }


    public static String getPlayerHeadOwner(ItemStack lot) {
        if (!isPlayerHead(lot)) {
            return null;
        }
        SkullMeta itemMeta = (SkullMeta) lot.getItemMeta();
        if (!itemMeta.hasOwner()) {
            return null;
        }
        return itemMeta.getOwningPlayer().getName();
    }


    public static void setPlayerHeadOwner(ItemStack item, String headName) {
        if (item == null) {
            return;
        }
        ItemMeta itemMeta = item.getItemMeta();
        if (itemMeta == null) {
            return;
        } else if (itemMeta instanceof SkullMeta) {
            SkullMeta skullMeta = ((SkullMeta) itemMeta);
            skullMeta.setOwner(headName);
            item.setItemMeta(skullMeta);
        }
    }

    private Items() {
    }
} @ApiStatus.Internal
public final class LegacyUtil {

    private static final boolean durabilityExists = methodExists(ItemStack.class, "getDurability");
    private static final boolean mainHandExists = methodExists(PlayerInventory.class, "getItemInMainHand");

    public static short getDurability(ItemStack itemStack) {
        if (durabilityExists) {
            return itemStack.getDurability();
        }
        return 0;
    }

    public static ItemStack getItemInMainHand(Player player) {
        if (mainHandExists) {
            return player.getInventory().getItemInMainHand();
        }
        return player.getItemInHand();
    }

    private static boolean methodExists(Class<?> clazz, String methodName) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return true;
            }
        }
        return false;
    }
}
import com.gmail.virustotalop.obsidianauctions.AuctionConfig;
import com.gmail.virustotalop.obsidianauctions.ObsidianAuctions;
import com.gmail.virustotalop.obsidianauctions.auction.Auction;
import com.gmail.virustotalop.obsidianauctions.auction.AuctionBid;
import com.gmail.virustotalop.obsidianauctions.auction.AuctionScope;
import com.gmail.virustotalop.obsidianauctions.language.TranslationFactory;
import com.gmail.virustotalop.obsidianauctions.placeholder.Placeholder;
import com.gmail.virustotalop.obsidianauctions.util.Functions;
import com.gmail.virustotalop.obsidianauctions.util.Items;
import com.gmail.virustotalop.obsidianauctions.util.LegacyUtil;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.FireworkEffect;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import javax.inject.Inject;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
package com.gmail.virustotalop.obsidianauctions.message;



@ApiStatus.Internal
public class AuctionMessageParser {

    private final TranslationFactory translation;
    private final Placeholder placeholder;

    @Inject
    private AuctionMessageParser(TranslationFactory translation, Placeholder placeholder) {
        this.translation = translation;
        this.placeholder = placeholder;
    }

    /**
     * Gets the messages from the language.yml file based on the keys passed in.
     *
     * @param messageKeys  Keys specified in the language.yml file
     * @param auctionScope A scope to check for local messages
     * @return List of actual messages to send
     */
    public List<String> parseMessages(List<String> messageKeys, AuctionScope auctionScope, Auction auction, Player player, boolean isBroadcast) {
        List<String> messageList = new ArrayList<>();

        for (String messageKey : messageKeys) {
            if (messageKey == null) {
                continue;
            }

            List<String> partialMessageList = AuctionConfig.getLanguageStringList(messageKey, auctionScope);

            if (partialMessageList == null || partialMessageList.size() == 0) {
                String originalMessage;
                originalMessage = AuctionConfig.getLanguageString(messageKey, auctionScope);

                if (originalMessage == null || originalMessage.length() == 0) {
                    continue;
                } else {
                    partialMessageList = Arrays.asList(originalMessage.split("(\r?\n|\r)"));
                }
            }
            messageList.addAll(partialMessageList);
        }
        return parseMessageTokens(messageList, auctionScope, auction, player, isBroadcast);
    }

    private List<String> parseMessageTokens(List<String> messageList, AuctionScope auctionScope, Auction auction, Player player, boolean isBroadcast) {
        List<String> newMessageList = new ArrayList<>();
        Map<String, String> replacements = new LinkedHashMap<>();
        ItemStack lot = null;

        if (auction == null && auctionScope != null) {
            auction = auctionScope.getActiveAuction();
        }

        // Search to see if auction info is required:
        for (String message : messageList) {
            if (message.length() > 0 && message.contains("%auction-")) {
                if (auction != null) {
                    replacements.put("%auction-owner-name%", auction.getOwnerName()); //%A1
                    replacements.put("%auction-owner-display-name%", auction.getOwnerDisplayName()); //%A2
                    replacements.put("%auction-quantity%", Integer.toString(auction.getLotQuantity()));
                    if (auction.getStartingBid() == 0) {
                        replacements.put("%auction-bid-starting%", Functions.formatAmount(auction.getMinBidIncrement())); //%A4
                    } else {
                        replacements.put("%auction-bid-starting%", Functions.formatAmount(auction.getStartingBid())); //%A4
                    }
                    replacements.put("%auction-bid-increment%", Functions.formatAmount(auction.getMinBidIncrement())); //%A5
                    replacements.put("%auction-buy-now%", Functions.formatAmount(auction.getBuyNow())); //%A6
                    replacements.put("%auction-remaining-time%", Functions.formatTime(auction.getRemainingTime(), auctionScope)); //%A7
                    replacements.put("%auction-pre-tax%", Functions.formatAmount(auction.getExtractedPreTax())); //%A8
                    replacements.put("%auction-post-tax%", Functions.formatAmount(auction.getExtractedPostTax())); //%A9
                }
                break;
            }
        }

        // Search to see if auction bid info is required:
        for (String message : messageList) {
            if (message.length() > 0 && (message.contains("%current-") || message.contains("%auction-bid"))) {
                if (auction != null) {
                    AuctionBid currentBid = auction.getCurrentBid();