public static final String DESCRIPTION = "Sponge jobs plugin."; public static final String ID = "jobslite"; public static final String NAME = "JobsLite"; public static final String VERSION = "@project.version@"; public interface JobManager {

    /**
     * Checks if a job exists.
     * 
     * @param job The job to check for.
     * @return If the job exists.
     */
    boolean jobExists(String job);

    /**
     * Gets a job.
     * 
     * @param job The job to get.
     * @return The job, if found.
     */
    Optional<Job> getJob(String job);

    /**
     * Gets all jobs.
     * 
     * @return All jobs.
     */
    List<Job> getJobs();

    /**
     * Creates a new job.
     * 
     * @param id The job id.
     * @param displayName The job display name.
     * @param maxLevel The max level of the job.
     * @param textColor The color of the job.
     * @param silkTouch If the job allows silk touch tools.
     * @param worldGen If the job allows only world generated blocks.
     * @param ignoreData If the job ignores block data.
     * @param blockBreaks The blocks that give rewards for breaking.
     * @param blockPlaces The blocks that give rewards for placing.
     * @param mobKills The mobs that give rewards for killing.
     * @return If the job was created successfully.
     */
    boolean createJob(String id, String displayName, int maxLevel, TextColor textColor, boolean silkTouch, boolean worldGen, boolean ignoreData,
            HashMap<BlockState, Reward> blockBreaks, HashMap<BlockState, Reward> blockPlaces, HashMap<EntityType, Reward> mobKills);

    /**
     * Deletes a job.
     * 
     * @param job The job to delete.
     * @return If the job was deleted successfully.
     */
    boolean deleteJob(String job);
} public interface PlayerManager {

    /**
     * Gets the current jobs of a player.
     * 
     * @param uuid The UUID of the player.
     * @return The current jobs of the player.
     */
    List<String> getCurrentJobs(UUID uuid);

    /**
     * Checks if a player has a specific job.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @return If the player has the job.
     */
    default boolean hasJob(UUID uuid, String job) {
        return getCurrentJobs(uuid).contains(job);
    }

    /**
     * Gets the current level of a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @return The current level of the player.
     */
    int getCurrentLevel(UUID uuid, String job);

    /**
     * Gets the current experience of a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @return The current experience of a player.
     */
    Optional<Double> getCurrentExp(UUID uuid, String job);

    /**
     * Sets the experience of a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @param exp The amount of experience.
     * @return If the method succeeded.
     */
    boolean setExp(UUID uuid, String job, double exp);

    /**
     * Sets the level of a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @param level The new level of the player.
     * @return If the method succeeded.
     */
    boolean setLevel(UUID uuid, String job, int level);

    /**
     * Adds a job to a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @return If the method succeeded.
     */
    boolean addJob(UUID uuid, String job);

    /**
     * Removes all jobs from a player.
     * 
     * @param uuid The UUID of the player.
     * @return If the method succeeded.
     */
    boolean removeAllJobs(UUID uuid);

    /**
     * Removes a job from a player.
     * 
     * @param uuid The UUID of the player.
     * @param job The name of the job.
     * @return If the method succeeded.
     */
    boolean removeJob(UUID uuid, String job);

    /**
     * Removes a job from all players.
     * 
     * @param job The job to remove.
     * @return If the method was successful.
     */
    boolean removeJob(String job);
} public class Metrics {

    // The version of this bStats class
    public static final int B_STATS_VERSION = 1;

    // The url to which the data is sent
    private static final String URL = "https://bStats.org/submitData/sponge";

    // We use this flag to ensure only one instance of this class exist
    private static boolean created = false;

    // The logger
    private Logger logger;

    // The plugin
    private final PluginContainer plugin;

    // Is bStats enabled on this server?
    private boolean enabled;

    // The uuid of the server
    private String serverUUID;

    // Should failed requests be logged?
    private boolean logFailedRequests = false;

    // A list with all known metrics class objects including this one
    private static final List<Object> knownMetricsInstances = new ArrayList<>();

    // A list with all custom charts
    private final List<CustomChart> charts = new ArrayList<>();

    // The config path
    private Path configDir;

    // The constructor is not meant to be called by the user himself.
    // The instance is created using Dependency Injection
    // (https://docs.spongepowered.org/master/en/plugin/injection.html)
    @Inject
    private Metrics(PluginContainer plugin, Logger logger, @ConfigDir(sharedRoot = true) Path configDir) {
        if (created) {
            // We don't want more than one instance of this class
            throw new IllegalStateException("There's already an instance of this Metrics class!");
        } else {
            created = true;
        }

        this.plugin = plugin;
        this.logger = logger;
        this.configDir = configDir;

        try {
            loadConfig();
        } catch (IOException e) {
            // Failed to load configuration
            logger.warn("Failed to load bStats config!", e);
            return;
        }

        // We are not allowed to send data about this server :(
        if (!enabled) {
            return;
        }

        Class<?> usedMetricsClass = getFirstBStatsClass();
        if (usedMetricsClass == null) {
            // Failed to get first metrics class
            return;
        }
        if (usedMetricsClass == getClass()) {
            // We are the first! :)
            linkMetrics(this);
            startSubmitting();
        } else {
            // We aren't the first so we link to the first metrics class
            try {
                usedMetricsClass.getMethod("linkMetrics", Object.class).invoke(null, this);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                if (logFailedRequests) {
                    logger.warn("Failed to link to first metrics class {}!", usedMetricsClass.getName(), e);
                }
            }
        }
    }

    /**
     * Adds a custom chart.
     *
     * @param chart The chart to add.
     */
    public void addCustomChart(CustomChart chart) {
        Validate.notNull(chart, "Chart cannot be null");
        charts.add(chart);
    }

    /**
     * Links an other metrics class with this class. This method is called using
     * Reflection.
     *
     * @param metrics An object of the metrics class to link.
     */
    public static void linkMetrics(Object metrics) {
        knownMetricsInstances.add(metrics);
    }

    /**
     * Gets the plugin specific data. This method is called using Reflection.
     *
     * @return The plugin specific data.
     */
    public JsonObject getPluginData() {
        JsonObject data = new JsonObject();

        String pluginName = plugin.getName();
        String pluginVersion = plugin.getVersion().orElse("unknown");

        data.addProperty("pluginName", pluginName);
        data.addProperty("pluginVersion", pluginVersion);

        JsonArray customCharts = new JsonArray();
        for (CustomChart customChart : charts) {
            // Add the data of the custom charts
            JsonObject chart = customChart.getRequestJsonObject(logger, logFailedRequests);
            if (chart == null) { // If the chart is null, we skip it
                continue;
            }
            customCharts.add(chart);
        }
        data.add("customCharts", customCharts);

        return data;
    }

    private void startSubmitting() {
        // We use a timer cause want to be independent from the server tps
        final Timer timer = new Timer(true);
        timer.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                // Plugin was disabled, e.g. because of a reload (is this even
                // possible in Sponge?)
                if (!Sponge.getPluginManager().isLoaded(plugin.getId())) {
                    timer.cancel();
                    return;
                }
                // The data collection (e.g. for custom graphs) is done sync
                // Don't be afraid! The connection to the bStats server is still
                // async, only the stats collection is sync ;)
                Scheduler scheduler = Sponge.getScheduler();
                Task.Builder taskBuilder = scheduler.createTaskBuilder();
                taskBuilder.execute(() -> submitData()).submit(plugin);
            }
        }, 1000 * 60 * 5, 1000 * 60 * 30);
        // Submit the data every 30 minutes, first time after 5 minutes to give
        // other plugins enough time to start
        // WARNING: Changing the frequency has no effect but your plugin WILL be
        // blocked/deleted!
        // WARNING: Just don't do it!
    }

    /**
     * Gets the server specific data.
     *
     * @return The server specific data.
     */
    private JsonObject getServerData() {
        // Minecraft specific data
        int playerAmount = Sponge.getServer().getOnlinePlayers().size();
        playerAmount = playerAmount > 200 ? 200 : playerAmount;
        int onlineMode = Sponge.getServer().getOnlineMode() ? 1 : 0;
        String minecraftVersion = Sponge.getGame().getPlatform().getMinecraftVersion().getName();

        // OS/Java specific data
        String javaVersion = System.getProperty("java.version");
        String osName = System.getProperty("os.name");
        String osArch = System.getProperty("os.arch");
        String osVersion = System.getProperty("os.version");
        int coreCount = Runtime.getRuntime().availableProcessors();

        JsonObject data = new JsonObject();

        data.addProperty("serverUUID", serverUUID);

        data.addProperty("playerAmount", playerAmount);
        data.addProperty("onlineMode", onlineMode);
        data.addProperty("minecraftVersion", minecraftVersion);

        data.addProperty("javaVersion", javaVersion);
        data.addProperty("osName", osName);
        data.addProperty("osArch", osArch);
        data.addProperty("osVersion", osVersion);
        data.addProperty("coreCount", coreCount);

        return data;
    }

    /**
     * Collects the data and sends it afterwards.
     */
    private void submitData() {
        final JsonObject data = getServerData();

        JsonArray pluginData = new JsonArray();
        // Search for all other bStats Metrics classes to get their plugin data
        for (Object metrics : knownMetricsInstances) {
            try {
                Object plugin = metrics.getClass().getMethod("getPluginData").invoke(metrics);
                if (plugin instanceof JsonObject) {
                    pluginData.add((JsonObject) plugin);
                }
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
            }
        }

        data.add("plugins", pluginData);

        // Create a new thread for the connection to the bStats server
        new Thread(() -> {
            try {
                // Send the data
                sendData(data);
            } catch (Exception e) {
                // Something went wrong! :(
                if (logFailedRequests) {
                    logger.warn("Could not submit plugin stats!", e);
                }
            }
        }).start();
    }

    /**
     * Loads the bStats configuration.
     *
     * @throws IOException If something did not work :(
     */
    private void loadConfig() throws IOException {
        Path configPath = configDir.resolve("bStats");
        configPath.toFile().mkdirs();
        File configFile = new File(configPath.toFile(), "config.conf");
        HoconConfigurationLoader configurationLoader = HoconConfigurationLoader.builder().setFile(configFile).build();
        CommentedConfigurationNode node;
        if (!configFile.exists()) {
            configFile.createNewFile();
            node = configurationLoader.load();

            // Add default values
            node.getNode("enabled").setValue(true);
            // Every server gets it's unique random id.
            node.getNode("serverUuid").setValue(UUID.randomUUID().toString());
            // Should failed request be logged?
            node.getNode("logFailedRequests").setValue(false);

            // Add information about bStats
            node.getNode("enabled").setComment(
                    "bStats collects some data for plugin authors like how many servers are using their plugins.\n" +
                            "To honor their work, you should not disable it.\n" +
                            "This has nearly no effect on the server performance!\n" +
                            "Check out https://bStats.org/ to learn more :)"
                    );

            configurationLoader.save(node);
        } else {
            node = configurationLoader.load();
        }

        // Load configuration
        enabled = node.getNode("enabled").getBoolean(true);
        serverUUID = node.getNode("serverUuid").getString();
        logFailedRequests = node.getNode("logFailedRequests").getBoolean(false);
    }

    /**
     * Gets the first bStat Metrics class.
     *
     * @return The first bStats metrics class.
     */
    private Class<?> getFirstBStatsClass() {
        Path configPath = configDir.resolve("bStats");
        configPath.toFile().mkdirs();
        File tempFile = new File(configPath.toFile(), "temp.txt");

        try {
            String className = readFile(tempFile);
            if (className != null) {
                try {
                    // Let's check if a class with the given name exists.
                    return Class.forName(className);
                } catch (ClassNotFoundException ignored) {
                }
            }
            writeFile(tempFile, getClass().getName());
            return getClass();
        } catch (IOException e) {
            if (logFailedRequests) {
                logger.warn("Failed to get first bStats class!", e);
            }
            return null;
        }
    }

    /**
     * Reads the first line of the file.
     *
     * @param file The file to read. Cannot be null.
     * @return The first line of the file or <code>null</code> if the file does
     *         not exist or is empty.
     * @throws IOException If something did not work :(
     */
    private String readFile(File file) throws IOException {
        if (!file.exists()) {
            return null;
        }
        try (
                FileReader fileReader = new FileReader(file);
                BufferedReader bufferedReader = new BufferedReader(fileReader);) {
            return bufferedReader.readLine();
        }
    }

    /**
     * Writes a String to a file. It also adds a note for the user,
     *
     * @param file The file to write to. Cannot be null.
     * @param text The text to write.
     * @throws IOException If something did not work :(
     */
    private void writeFile(File file, String text) throws IOException {
        if (!file.exists()) {
            file.createNewFile();
        }
        try (
                FileWriter fileWriter = new FileWriter(file);
                BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) {
            bufferedWriter.write(text);
            bufferedWriter.newLine();
            bufferedWriter.write("Note: This class only exists for internal purpose. You can ignore it :)");
        }
    }

    /**
     * Sends the data to the bStats server.
     *
     * @param data The data to send.
     * @throws Exception If the request failed.
     */
    private static void sendData(JsonObject data) throws Exception {
        Validate.notNull(data, "Data cannot be null");
        HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();

        // Compress the data to save bandwidth
        byte[] compressedData = compress(data.toString());

        // Add headers
        connection.setRequestMethod("POST");
        connection.addRequestProperty("Accept", "application/json");
        connection.addRequestProperty("Connection", "close");
        connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip
                                                                   // our
                                                                   // request
        connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
        connection.setRequestProperty("Content-Type", "application/json"); // We
                                                                           // send
                                                                           // our
                                                                           // data
                                                                           // in
                                                                           // JSON
                                                                           // format
        connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);

        // Send data
        connection.setDoOutput(true);
        DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
        outputStream.write(compressedData);
        outputStream.flush();
        outputStream.close();

        connection.getInputStream().close(); // We don't care about the response
                                             // - Just send our data :)
    }

    /**
     * Gzips the given String.
     *
     * @param str The string to gzip.
     * @return The gzipped String.
     * @throws IOException If the compression failed.
     */
    private static byte[] compress(final String str) throws IOException {
        if (str == null) {
            return null;
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
        gzip.write(str.getBytes("UTF-8"));
        gzip.close();
        return outputStream.toByteArray();
    }

    /**
     * Represents a custom chart.
     */
    public static abstract class CustomChart {

        // The id of the chart
        protected final String chartId;

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public CustomChart(String chartId) {
            if (chartId == null || chartId.isEmpty()) {
                throw new IllegalArgumentException("ChartId cannot be null or empty!");
            }
            this.chartId = chartId;
        }

        protected JsonObject getRequestJsonObject(Logger logger, boolean logFailedRequests) {
            JsonObject chart = new JsonObject();
            chart.addProperty("chartId", chartId);
            try {
                JsonObject data = getChartData();
                if (data == null) {
                    // If the data is null we don't send the chart.
                    return null;
                }
                chart.add("data", data);
            } catch (Throwable t) {
                if (logFailedRequests) {
                    logger.warn("Failed to get data for custom chart with id {}", chartId, t);
                }
                return null;
            }
            return chart;
        }

        protected abstract JsonObject getChartData();

    }

    /**
     * Represents a custom simple pie.
     */
    public static abstract class SimplePie extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public SimplePie(String chartId) {
            super(chartId);
        }

        /**
         * Gets the value of the pie.
         *
         * @return The value of the pie.
         */
        public abstract String getValue();

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            String value = getValue();
            if (value == null || value.isEmpty()) {
                // Null = skip the chart
                return null;
            }
            data.addProperty("value", value);
            return data;
        }
    }

    /**
     * Represents a custom advanced pie.
     */
    public static abstract class AdvancedPie extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public AdvancedPie(String chartId) {
            super(chartId);
        }

        /**
         * Gets the values of the pie.
         *
         * @param valueMap Just an empty map. The only reason it exists is to
         *        make your life easier. You don't have to create a map
         *        yourself!
         * @return The values of the pie.
         */
        public abstract HashMap<String, Integer> getValues(HashMap<String, Integer> valueMap);

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            JsonObject values = new JsonObject();
            HashMap<String, Integer> map = getValues(new HashMap<String, Integer>());
            if (map == null || map.isEmpty()) {
                // Null = skip the chart
                return null;
            }
            boolean allSkipped = true;
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue() == 0) {
                    continue; // Skip this invalid
                }
                allSkipped = false;
                values.addProperty(entry.getKey(), entry.getValue());
            }
            if (allSkipped) {
                // Null = skip the chart
                return null;
            }
            data.add("values", values);
            return data;
        }
    }

    /**
     * Represents a custom single line chart.
     */
    public static abstract class SingleLineChart extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public SingleLineChart(String chartId) {
            super(chartId);
        }

        /**
         * Gets the value of the chart.
         *
         * @return The value of the chart.
         */
        public abstract int getValue();

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            int value = getValue();
            if (value == 0) {
                // Null = skip the chart
                return null;
            }
            data.addProperty("value", value);
            return data;
        }

    }

    /**
     * Represents a custom multi line chart.
     */
    public static abstract class MultiLineChart extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public MultiLineChart(String chartId) {
            super(chartId);
        }

        /**
         * Gets the values of the chart.
         *
         * @param valueMap Just an empty map. The only reason it exists is to
         *        make your life easier. You don't have to create a map
         *        yourself!
         * @return The values of the chart.
         */
        public abstract HashMap<String, Integer> getValues(HashMap<String, Integer> valueMap);

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            JsonObject values = new JsonObject();
            HashMap<String, Integer> map = getValues(new HashMap<String, Integer>());
            if (map == null || map.isEmpty()) {
                // Null = skip the chart
                return null;
            }
            boolean allSkipped = true;
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue() == 0) {
                    continue; // Skip this invalid
                }
                allSkipped = false;
                values.addProperty(entry.getKey(), entry.getValue());
            }
            if (allSkipped) {
                // Null = skip the chart
                return null;
            }
            data.add("values", values);
            return data;
        }

    }

    /**
     * Represents a custom simple map chart.
     */
    public static abstract class SimpleMapChart extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public SimpleMapChart(String chartId) {
            super(chartId);
        }

        /**
         * Gets the value of the chart.
         *
         * @return The value of the chart.
         */
        public abstract Country getValue();

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            Country value = getValue();

            if (value == null) {
                // Null = skip the chart
                return null;
            }
            data.addProperty("value", value.getCountryIsoTag());
            return data;
        }

    }

    /**
     * Represents a custom advanced map chart.
     */
    public static abstract class AdvancedMapChart extends CustomChart {

        /**
         * Class constructor.
         *
         * @param chartId The id of the chart.
         */
        public AdvancedMapChart(String chartId) {
            super(chartId);
        }

        /**
         * Gets the value of the chart.
         *
         * @param valueMap Just an empty map. The only reason it exists is to
         *        make your life easier. You don't have to create a map
         *        yourself!
         * @return The value of the chart.
         */
        public abstract HashMap<Country, Integer> getValues(HashMap<Country, Integer> valueMap);

        @Override
        protected JsonObject getChartData() {
            JsonObject data = new JsonObject();
            JsonObject values = new JsonObject();
            HashMap<Country, Integer> map = getValues(new HashMap<Country, Integer>());
            if (map == null || map.isEmpty()) {
                // Null = skip the chart
                return null;
            }
            boolean allSkipped = true;
            for (Map.Entry<Country, Integer> entry : map.entrySet()) {
                if (entry.getValue() == 0) {
                    continue; // Skip this invalid
                }
                allSkipped = false;
                values.addProperty(entry.getKey().getCountryIsoTag(), entry.getValue());
            }
            if (allSkipped) {
                // Null = skip the chart
                return null;
            }
            data.add("values", values);
            return data;
        }

    }

    /**
     * A enum which is used for custom maps.
     */
    public enum Country {

        /**
         * bStats will use the country of the server.
         */
        AUTO_DETECT("AUTO", "Auto Detected"),

        ANDORRA("AD", "Andorra"),
        UNITED_ARAB_EMIRATES("AE", "United Arab Emirates"),
        AFGHANISTAN("AF", "Afghanistan"),
        ANTIGUA_AND_BARBUDA("AG", "Antigua and Barbuda"),
        ANGUILLA("AI", "Anguilla"),
        ALBANIA("AL", "Albania"),
        ARMENIA("AM", "Armenia"),
        NETHERLANDS_ANTILLES("AN", "Netherlands Antilles"),
        ANGOLA("AO", "Angola"),
        ANTARCTICA("AQ", "Antarctica"),
        ARGENTINA("AR", "Argentina"),
        AMERICAN_SAMOA("AS", "American Samoa"),
        AUSTRIA("AT", "Austria"),
        AUSTRALIA("AU", "Australia"),
        ARUBA("AW", "Aruba"),
        ALAND_ISLANDS("AX", "Aland Islands"),
        AZERBAIJAN("AZ", "Azerbaijan"),
        BOSNIA_AND_HERZEGOVINA("BA", "Bosnia and Herzegovina"),
        BARBADOS("BB", "Barbados"),
        BANGLADESH("BD", "Bangladesh"),
        BELGIUM("BE", "Belgium"),
        BURKINA_FASO("BF", "Burkina Faso"),
        BULGARIA("BG", "Bulgaria"),
        BAHRAIN("BH", "Bahrain"),
        BURUNDI("BI", "Burundi"),
        BENIN("BJ", "Benin"),
        SAINT_BARTHELEMY("BL", "Saint Barthelemy"),
        BERMUDA("BM", "Bermuda"),
        BRUNEI("BN", "Brunei"),
        BOLIVIA("BO", "Bolivia"),
        BONAIRE_SINT_EUSTATIUS_AND_SABA("BQ", "Bonaire, Sint Eustatius and Saba"),
        BRAZIL("BR", "Brazil"),
        BAHAMAS("BS", "Bahamas"),
        BHUTAN("BT", "Bhutan"),
        BOUVET_ISLAND("BV", "Bouvet Island"),
        BOTSWANA("BW", "Botswana"),
        BELARUS("BY", "Belarus"),
        BELIZE("BZ", "Belize"),
        CANADA("CA", "Canada"),
        COCOS_ISLANDS("CC", "Cocos Islands"),
        THE_DEMOCRATIC_REPUBLIC_OF_CONGO("CD", "The Democratic Republic Of Congo"),
        CENTRAL_AFRICAN_REPUBLIC("CF", "Central African Republic"),
        CONGO("CG", "Congo"),
        SWITZERLAND("CH", "Switzerland"),
        COTE_D_IVOIRE("CI", "Cote d'Ivoire"),
        COOK_ISLANDS("CK", "Cook Islands"),
        CHILE("CL", "Chile"),
        CAMEROON("CM", "Cameroon"),
        CHINA("CN", "China"),
        COLOMBIA("CO", "Colombia"),
        COSTA_RICA("CR", "Costa Rica"),
        CUBA("CU", "Cuba"),
        CAPE_VERDE("CV", "Cape Verde"),
        CURACAO("CW", "Curacao"),
        CHRISTMAS_ISLAND("CX", "Christmas Island"),
        CYPRUS("CY", "Cyprus"),
        CZECH_REPUBLIC("CZ", "Czech Republic"),
        GERMANY("DE", "Germany"),
        DJIBOUTI("DJ", "Djibouti"),
        DENMARK("DK", "Denmark"),
        DOMINICA("DM", "Dominica"),
        DOMINICAN_REPUBLIC("DO", "Dominican Republic"),
        ALGERIA("DZ", "Algeria"),
        ECUADOR("EC", "Ecuador"),
        ESTONIA("EE", "Estonia"),
        EGYPT("EG", "Egypt"),
        WESTERN_SAHARA("EH", "Western Sahara"),
        ERITREA("ER", "Eritrea"),
        SPAIN("ES", "Spain"),
        ETHIOPIA("ET", "Ethiopia"),
        FINLAND("FI", "Finland"),
        FIJI("FJ", "Fiji"),
        FALKLAND_ISLANDS("FK", "Falkland Islands"),
        MICRONESIA("FM", "Micronesia"),
        FAROE_ISLANDS("FO", "Faroe Islands"),
        FRANCE("FR", "France"),
        GABON("GA", "Gabon"),
        UNITED_KINGDOM("GB", "United Kingdom"),
        GRENADA("GD", "Grenada"),
        GEORGIA("GE", "Georgia"),
        FRENCH_GUIANA("GF", "French Guiana"),
        GUERNSEY("GG", "Guernsey"),
        GHANA("GH", "Ghana"),
        GIBRALTAR("GI", "Gibraltar"),
        GREENLAND("GL", "Greenland"),
        GAMBIA("GM", "Gambia"),
        GUINEA("GN", "Guinea"),
        GUADELOUPE("GP", "Guadeloupe"),
        EQUATORIAL_GUINEA("GQ", "Equatorial Guinea"),
        GREECE("GR", "Greece"),
        SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS("GS", "South Georgia And The South Sandwich Islands"),
        GUATEMALA("GT", "Guatemala"),
        GUAM("GU", "Guam"),
        GUINEA_BISSAU("GW", "Guinea-Bissau"),
        GUYANA("GY", "Guyana"),
        HONG_KONG("HK", "Hong Kong"),
        HEARD_ISLAND_AND_MCDONALD_ISLANDS("HM", "Heard Island And McDonald Islands"),
        HONDURAS("HN", "Honduras"),
        CROATIA("HR", "Croatia"),
        HAITI("HT", "Haiti"),
        HUNGARY("HU", "Hungary"),
        INDONESIA("ID", "Indonesia"),
        IRELAND("IE", "Ireland"),
        ISRAEL("IL", "Israel"),
        ISLE_OF_MAN("IM", "Isle Of Man"),
        INDIA("IN", "India"),
        BRITISH_INDIAN_OCEAN_TERRITORY("IO", "British Indian Ocean Territory"),
        IRAQ("IQ", "Iraq"),
        IRAN("IR", "Iran"),
        ICELAND("IS", "Iceland"),
        ITALY("IT", "Italy"),
        JERSEY("JE", "Jersey"),
        JAMAICA("JM", "Jamaica"),
        JORDAN("JO", "Jordan"),
        JAPAN("JP", "Japan"),
        KENYA("KE", "Kenya"),
        KYRGYZSTAN("KG", "Kyrgyzstan"),
        CAMBODIA("KH", "Cambodia"),
        KIRIBATI("KI", "Kiribati"),
        COMOROS("KM", "Comoros"),
        SAINT_KITTS_AND_NEVIS("KN", "Saint Kitts And Nevis"),
        NORTH_KOREA("KP", "North Korea"),
        SOUTH_KOREA("KR", "South Korea"),
        KUWAIT("KW", "Kuwait"),
        CAYMAN_ISLANDS("KY", "Cayman Islands"),
        KAZAKHSTAN("KZ", "Kazakhstan"),
        LAOS("LA", "Laos"),
        LEBANON("LB", "Lebanon"),
        SAINT_LUCIA("LC", "Saint Lucia"),
        LIECHTENSTEIN("LI", "Liechtenstein"),
        SRI_LANKA("LK", "Sri Lanka"),
        LIBERIA("LR", "Liberia"),
        LESOTHO("LS", "Lesotho"),
        LITHUANIA("LT", "Lithuania"),
        LUXEMBOURG("LU", "Luxembourg"),
        LATVIA("LV", "Latvia"),
        LIBYA("LY", "Libya"),
        MOROCCO("MA", "Morocco"),
        MONACO("MC", "Monaco"),
        MOLDOVA("MD", "Moldova"),
        MONTENEGRO("ME", "Montenegro"),
        SAINT_MARTIN("MF", "Saint Martin"),
        MADAGASCAR("MG", "Madagascar"),
        MARSHALL_ISLANDS("MH", "Marshall Islands"),
        MACEDONIA("MK", "Macedonia"),
        MALI("ML", "Mali"),
        MYANMAR("MM", "Myanmar"),
        MONGOLIA("MN", "Mongolia"),
        MACAO("MO", "Macao"),
        NORTHERN_MARIANA_ISLANDS("MP", "Northern Mariana Islands"),
        MARTINIQUE("MQ", "Martinique"),
        MAURITANIA("MR", "Mauritania"),
        MONTSERRAT("MS", "Montserrat"),
        MALTA("MT", "Malta"),
        MAURITIUS("MU", "Mauritius"),
        MALDIVES("MV", "Maldives"),
        MALAWI("MW", "Malawi"),
        MEXICO("MX", "Mexico"),
        MALAYSIA("MY", "Malaysia"),
        MOZAMBIQUE("MZ", "Mozambique"),
        NAMIBIA("NA", "Namibia"),
        NEW_CALEDONIA("NC", "New Caledonia"),
        NIGER("NE", "Niger"),
        NORFOLK_ISLAND("NF", "Norfolk Island"),
        NIGERIA("NG", "Nigeria"),
        NICARAGUA("NI", "Nicaragua"),
        NETHERLANDS("NL", "Netherlands"),
        NORWAY("NO", "Norway"),
        NEPAL("NP", "Nepal"),
        NAURU("NR", "Nauru"),
        NIUE("NU", "Niue"),
        NEW_ZEALAND("NZ", "New Zealand"),
        OMAN("OM", "Oman"),
        PANAMA("PA", "Panama"),
        PERU("PE", "Peru"),
        FRENCH_POLYNESIA("PF", "French Polynesia"),
        PAPUA_NEW_GUINEA("PG", "Papua New Guinea"),
        PHILIPPINES("PH", "Philippines"),
        PAKISTAN("PK", "Pakistan"),
        POLAND("PL", "Poland"),
        SAINT_PIERRE_AND_MIQUELON("PM", "Saint Pierre And Miquelon"),
        PITCAIRN("PN", "Pitcairn"),
        PUERTO_RICO("PR", "Puerto Rico"),
        PALESTINE("PS", "Palestine"),
        PORTUGAL("PT", "Portugal"),
        PALAU("PW", "Palau"),
        PARAGUAY("PY", "Paraguay"),
        QATAR("QA", "Qatar"),
        REUNION("RE", "Reunion"),
        ROMANIA("RO", "Romania"),
        SERBIA("RS", "Serbia"),
        RUSSIA("RU", "Russia"),
        RWANDA("RW", "Rwanda"),
        SAUDI_ARABIA("SA", "Saudi Arabia"),
        SOLOMON_ISLANDS("SB", "Solomon Islands"),
        SEYCHELLES("SC", "Seychelles"),
        SUDAN("SD", "Sudan"),
        SWEDEN("SE", "Sweden"),
        SINGAPORE("SG", "Singapore"),
        SAINT_HELENA("SH", "Saint Helena"),
        SLOVENIA("SI", "Slovenia"),
        SVALBARD_AND_JAN_MAYEN("SJ", "Svalbard And Jan Mayen"),
        SLOVAKIA("SK", "Slovakia"),
        SIERRA_LEONE("SL", "Sierra Leone"),
        SAN_MARINO("SM", "San Marino"),
        SENEGAL("SN", "Senegal"),
        SOMALIA("SO", "Somalia"),
        SURINAME("SR", "Suriname"),
        SOUTH_SUDAN("SS", "South Sudan"),
        SAO_TOME_AND_PRINCIPE("ST", "Sao Tome And Principe"),
        EL_SALVADOR("SV", "El Salvador"),
        SINT_MAARTEN_DUTCH_PART("SX", "Sint Maarten (Dutch part)"),
        SYRIA("SY", "Syria"),
        SWAZILAND("SZ", "Swaziland"),
        TURKS_AND_CAICOS_ISLANDS("TC", "Turks And Caicos Islands"),
        CHAD("TD", "Chad"),
        FRENCH_SOUTHERN_TERRITORIES("TF", "French Southern Territories"),
        TOGO("TG", "Togo"),
        THAILAND("TH", "Thailand"),
        TAJIKISTAN("TJ", "Tajikistan"),
        TOKELAU("TK", "Tokelau"),
        TIMOR_LESTE("TL", "Timor-Leste"),
        TURKMENISTAN("TM", "Turkmenistan"),
        TUNISIA("TN", "Tunisia"),
        TONGA("TO", "Tonga"),
        TURKEY("TR", "Turkey"),
        TRINIDAD_AND_TOBAGO("TT", "Trinidad and Tobago"),
        TUVALU("TV", "Tuvalu"),
        TAIWAN("TW", "Taiwan"),
        TANZANIA("TZ", "Tanzania"),
        UKRAINE("UA", "Ukraine"),
        UGANDA("UG", "Uganda"),
        UNITED_STATES_MINOR_OUTLYING_ISLANDS("UM", "United States Minor Outlying Islands"),
        UNITED_STATES("US", "United States"),
        URUGUAY("UY", "Uruguay"),
        UZBEKISTAN("UZ", "Uzbekistan"),
        VATICAN("VA", "Vatican"),
        SAINT_VINCENT_AND_THE_GRENADINES("VC", "Saint Vincent And The Grenadines"),
        VENEZUELA("VE", "Venezuela"),
        BRITISH_VIRGIN_ISLANDS("VG", "British Virgin Islands"),
        U_S__VIRGIN_ISLANDS("VI", "U.S. Virgin Islands"),
        VIETNAM("VN", "Vietnam"),
        VANUATU("VU", "Vanuatu"),
        WALLIS_AND_FUTUNA("WF", "Wallis And Futuna"),
        SAMOA("WS", "Samoa"),
        YEMEN("YE", "Yemen"),
        MAYOTTE("YT", "Mayotte"),
        SOUTH_AFRICA("ZA", "South Africa"),
        ZAMBIA("ZM", "Zambia"),
        ZIMBABWE("ZW", "Zimbabwe");

        private String isoTag;
        private String name;

        Country(String isoTag, String name) {
            this.isoTag = isoTag;
            this.name = name;
        }

        /**
         * Gets the name of the country.
         *
         * @return The name of the country.
         */
        public String getCountryName() {
            return name;
        }

        /**
         * Gets the iso tag of the country.
         *
         * @return The iso tag of the country.
         */
        public String getCountryIsoTag() {
            return isoTag;
        }

        /**
         * Gets a country by it's iso tag.
         *
         * @param isoTag The iso tag of the county.
         * @return The country with the given iso tag or <code>null</code> if
         *         unknown.
         */
        public static Country byIsoTag(String isoTag) {
            for (Country country : Country.values()) {
                if (country.getCountryIsoTag().equals(isoTag)) {
                    return country;
                }
            }
            return null;
        }

        /**
         * Gets a country by a locale.
         *
         * @param locale The locale.
         * @return The country from the giben locale or <code>null</code> if
         *         unknown country or if the locale does not contain a country.
         */
        public static Country byLocale(Locale locale) {
            return byIsoTag(locale.getCountry());
        }

    }

} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"add"}, permission = "jobs.admin.add")
public class AddCommand extends BaseCommandExecutor<Player> {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .arguments(GenericArguments.string(Text.of("player")))
                .description(messageStorage.getMessage("command.add.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        Optional<String> target = args.<String>getOne("player");
        if (target.isPresent()) {
            String targetName = target.get();
            Optional<UserStorageService> sOpt = Sponge.getServiceManager().provide(UserStorageService.class);
            if (sOpt.isPresent()) {
                UserStorageService userStorage = sOpt.get();
                Optional<User> uOpt = userStorage.get(targetName);
                if (uOpt.isPresent()) {
                    Optional<Player> pOpt = uOpt.get().getPlayer();
                    if (!pOpt.isPresent()) {
                        player.sendMessage(messageStorage.getMessage("generic.error"));
                        return;
                    }
                    Player targetPlayer = pOpt.get();
                    String playerName = targetPlayer.getName();
                    UUID uuid = targetPlayer.getUniqueId();
                    List<String> curJobs = playerManager.getCurrentJobs(uuid);
                    if (curJobs.size() >= Integer.parseInt(JobsLite.getOption("max-jobs"))) {
                        player.sendMessage(messageStorage.getMessage("command.add.toomany", "player", playerName));
                        return;
                    }
                    player.sendMessage(messageStorage.getMessage("command.add.select", "player", playerName));
                    for (Job job : jobManager.getJobs()) {
                        String displayName = job.getDisplayName();
                        player.sendMessage(TextUtils.option(new Consumer<CommandSource>() {

                            @Override
                            public void accept(CommandSource source) {
                                if (curJobs.contains(job.getId())) {
                                    player.sendMessage(messageStorage.getMessage("command.add.already", "job", job.getDisplayName(), "player",
                                            playerName));
                                    return;
                                }
                                player.sendMessage(messageStorage.getMessage("command.add.confirm", "job", job.getDisplayName(), "player",
                                        playerName));
                                player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                                    @Override
                                    public void accept(CommandSource source) {
                                        if (!playerManager.addJob(uuid, job.getId())) {
                                            player.sendMessage(messageStorage.getMessage("generic.error"));
                                            return;
                                        }
                                        player.sendMessage(messageStorage.getMessage("command.add.success", "job", job.getDisplayName(), "player",
                                                playerName));
                                    }

                                }));
                            }

                        }, job.getTextColor(), displayName));
                    }
                } else {
                    player.sendMessage(messageStorage.getMessage("generic.error"));
                }
            } else {
                player.sendMessage(messageStorage.getMessage("generic.error"));
            }
        } else {
            player.sendMessage(messageStorage.getMessage("command.usage", "command", "/jobs", "subcommands", "set <player>"));
        }
    }
} @ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"create"}, permission = "jobs.admin.create")
public class CreateCommand extends BaseCommandExecutor<Player> {

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(JobsLite.getMessageStorage().getMessage("command.create.description"));
    }

    @Override
    public void run(Player src, CommandContext args) {
        new CreatingJob(src.getUniqueId());
    }

} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"delete", "del"}, permission = "jobs.admin.delete")
public class DeleteCommand extends BaseCommandExecutor<Player> {

    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(messageStorage.getMessage("command.delete.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        player.sendMessage(messageStorage.getMessage("command.delete.select"));
        for (Job job : jobManager.getJobs()) {
            String displayName = job.getDisplayName();
            if (!displayName.isEmpty()) {
                player.sendMessage(TextUtils.option(new Consumer<CommandSource>() {

                    @Override
                    public void accept(CommandSource source) {
                        player.sendMessage(messageStorage.getMessage("command.delete.confirm", "job", displayName));
                        player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                            @Override
                            public void accept(CommandSource source) {
                                jobManager.deleteJob(job.getId());
                                player.sendMessage(messageStorage.getMessage("command.delete.success", "job", displayName));
                            }

                        }));
                        player.sendMessage(TextUtils.noOption(new Consumer<CommandSource>() {

                            @Override
                            public void accept(CommandSource source) {
                                player.sendMessage(messageStorage.getMessage("command.delete.cancelled"));
                            }

                        }));
                    }

                }, job.getTextColor(), displayName));
            }
        }
    }
} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"info"}, permission = "jobs.user.info")
public class InfoCommand extends BaseCommandExecutor<Player> {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(messageStorage.getMessage("command.info.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        UUID uuid = player.getUniqueId();
        boolean first = true;
        for (String jobString : playerManager.getCurrentJobs(uuid)) {
            Optional<Job> jOpt = jobManager.getJob(jobString);
            if (jOpt.isPresent()) {
                Job job = jOpt.get();
                int lvl = playerManager.getCurrentLevel(uuid, jobString);
                if (!first) {
                    player.sendMessage(Text.of(""));
                }
                player.sendMessage(messageStorage.getMessage("command.info.job", "job", job.getDisplayName()));
                player.sendMessage(messageStorage.getMessage("command.info.level", "level", lvl + ""));
                Optional<Double> eOpt = playerManager.getCurrentExp(uuid, jobString);
                if (eOpt.isPresent()) {
                    String exp = DecimalFormat.getInstance().format(job.getExpRequired(lvl, eOpt.get()));
                    player.sendMessage(messageStorage.getMessage("command.info.exp", "exp", exp));
                }
                first = false;
            }
        }

        if (first) {
            player.sendMessage(messageStorage.getMessage("command.info.nojob"));
        }
    }
} @AsyncCommand
@Command(aliases = {"jobs"}, permission = "jobs.user.command")
public class JobsCommand extends BaseCommandExecutor<Player> {

    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(messageStorage.getMessage("command.jobs.description"));
    }

    @Override
    public void run(Player src, CommandContext args) {
        Map<String, String> perms = Maps.newHashMap(ImmutableMap.of("jobs.user.info", "info", "jobs.user.join", "join", "jobs.user.leave", "leave"));
        Map<String, String> adminPerms =
                ImmutableMap.of("jobs.admin.add", "add", "jobs.admin.create", "create", "jobs.admin.delete", "delete", "jobs.admin.remove", "remove");
        perms.putAll(adminPerms);
        String commands = "";
        // Loop through the permissions
        boolean first = true;
        for (Entry<String, String> entry : perms.entrySet()) {
            String perm = entry.getKey();
            String command = entry.getValue();
            if (src.hasPermission(perm)) {
                if (first) {
                    commands += command;
                } else {
                    commands += " | " + command;
                }
            }
            first = false;
        }
        src.sendMessage(messageStorage.getMessage("command.usage", "command", "/jobs", "subcommands", commands));
    }
} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"join"}, permission = "jobs.user.join")
public class JoinCommand extends BaseCommandExecutor<Player> {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(messageStorage.getMessage("command.join.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        UUID uuid = player.getUniqueId();
        List<String> curJobs = playerManager.getCurrentJobs(uuid);
        if (curJobs.size() >= Integer.parseInt(JobsLite.getOption("max-jobs"))) {
            player.sendMessage(messageStorage.getMessage("command.join.toomany"));
            return;
        }
        player.sendMessage(messageStorage.getMessage("command.join.select"));
        for (Job job : jobManager.getJobs()) {
            String displayName = job.getDisplayName();
            player.sendMessage(TextUtils.option(new Consumer<CommandSource>() {

                @Override
                public void accept(CommandSource source) {
                    List<String> curJobs = playerManager.getCurrentJobs(uuid);
                    if (curJobs.contains(job.getId())) {
                        player.sendMessage(messageStorage.getMessage("command.join.already", "job", job.getDisplayName()));
                        return;
                    }
                    
                    if (curJobs.size() >= Integer.parseInt(JobsLite.getOption("max-jobs"))) {
                        player.sendMessage(messageStorage.getMessage("command.join.toomany"));
                        return;
                    }
                    
                    player.sendMessage(messageStorage.getMessage("command.join.confirm", "job", job.getDisplayName()));
                    player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                        @Override
                        public void accept(CommandSource source) {
                            if (!playerManager.addJob(uuid, job.getId())) {
                                player.sendMessage(messageStorage.getMessage("generic.error"));
                                return;
                            }
                            player.sendMessage(messageStorage.getMessage("command.join.success", "job", job.getDisplayName()));
                        }

                    }));
                }

            }, job.getTextColor(), displayName));
        }
    }
} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"leave", "quit", "exit"}, permission = "jobs.user.leave")
public class LeaveCommand extends BaseCommandExecutor<Player> {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .description(messageStorage.getMessage("command.leave.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        UUID uuid = player.getUniqueId();
        player.sendMessage(messageStorage.getMessage("command.leave.warn"));
        for (String jobId : playerManager.getCurrentJobs(uuid)) {
            Optional<Job> jOpt = jobManager.getJob(jobId);
            if (jOpt.isPresent()) {
                Job job = jOpt.get();
                player.sendMessage(TextUtils.option(new Consumer<CommandSource>() {

                    @Override
                    public void accept(CommandSource source) {
                        player.sendMessage(messageStorage.getMessage("command.leave.confirm", "job", job.getDisplayName()));
                        player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                            @Override
                            public void accept(CommandSource source) {
                                if (!playerManager.removeJob(uuid, job.getId())) {
                                    player.sendMessage(messageStorage.getMessage("generic.error"));
                                    return;
                                }
                                player.sendMessage(messageStorage.getMessage("command.leave.success", "job", job.getDisplayName()));
                            }

                        }));
                    }

                }, job.getTextColor(), job.getDisplayName()));
            }
        }
    }

} @AsyncCommand
@ParentCommand(parentCommand = JobsCommand.class)
@Command(aliases = {"remove"}, permission = "jobs.admin.remove")
public class RemoveCommand extends BaseCommandExecutor<Player> {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Override
    public Builder getCommandSpecBuilder() {
        return CommandSpec.builder()
                .executor(this)
                .arguments(GenericArguments.user(Text.of("player")))
                .description(messageStorage.getMessage("command.remove.description"));
    }

    @Override
    public void run(Player player, CommandContext args) {
        Optional<User> target = args.<User>getOne("player");
        if (target.isPresent()) {
            User user = target.get();
            String playerName = user.getName();
            UUID uuid = user.getUniqueId();
            player.sendMessage(messageStorage.getMessage("command.remove.warn", "player", playerName));
            for (String jobId : playerManager.getCurrentJobs(uuid)) {
                Optional<Job> jOpt = jobManager.getJob(jobId);
                if (jOpt.isPresent()) {
                    Job job = jOpt.get();
                    player.sendMessage(TextUtils.option(new Consumer<CommandSource>() {

                        @Override
                        public void accept(CommandSource source) {
                            player.sendMessage(messageStorage.getMessage("command.remove.confirm", "job", job.getDisplayName(), "player", playerName));
                            player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                                @Override
                                public void accept(CommandSource source) {
                                    if (!playerManager.removeJob(uuid, job.getId())) {
                                        player.sendMessage(messageStorage.getMessage("generic.error"));
                                        return;
                                    }
                                    player.sendMessage(messageStorage.getMessage("command.remove.success", "player", playerName, "job",
                                            job.getDisplayName()));
                                }

                            }));
                        }

                    }, job.getTextColor(), job.getDisplayName()));
                }
            }
        } else {
            player.sendMessage(messageStorage.getMessage("generic.error"));
        }
    }
} @Deprecated
public class ImmutableJobData extends AbstractImmutableData<ImmutableJobData, JobData> implements Comparable<ImmutableJobData> {

    private int level;
    private int exp;
    private String jobName;

    protected ImmutableJobData(String jobName, int level, int exp) {
        this.jobName = jobName;
        this.level = level;
        this.exp = exp;
        registerGetters();
    }

    public ImmutableValue<String> jobName() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.JOB_NAME, this.jobName, "").asImmutable();
    }

    public ImmutableValue<Integer> level() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.LEVEL, this.level, 0).asImmutable();
    }

    public ImmutableValue<Integer> exp() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.EXP, this.exp, 0).asImmutable();
    }

    @Override
    protected void registerGetters() {
        registerFieldGetter(LiteKeys.LEVEL, () -> this.level);
        registerKeyValue(LiteKeys.LEVEL, this::level);

        registerFieldGetter(LiteKeys.EXP, () -> this.exp);
        registerKeyValue(LiteKeys.EXP, this::exp);

        registerFieldGetter(LiteKeys.JOB_NAME, () -> this.jobName);
        registerKeyValue(LiteKeys.JOB_NAME, this::jobName);
    }

    @Override
    public <E> Optional<ImmutableJobData> with(Key<? extends BaseValue<E>> key, E value) {
        return Optional.empty();
    }

    @Override
    public int compareTo(ImmutableJobData o) {
        return ComparisonChain.start()
                .compare(o.jobName, this.jobName)
                .compare(o.level, this.level)
                .compare(o.exp, this.exp)
                .result();
    }

    @Override
    public int getContentVersion() {
        return 1;
    }

    @Override
    public DataContainer toContainer() {
        return super.toContainer()
                .set(LiteKeys.JOB_NAME, this.jobName)
                .set(LiteKeys.LEVEL, this.level)
                .set(LiteKeys.EXP, this.exp);
    }

    @Override
    public JobData asMutable() {
        return new JobData(jobName, level, exp);
    }

} public class ImmutableSignJobData extends AbstractImmutableData<ImmutableSignJobData, SignJobData> implements Comparable<ImmutableSignJobData> {

    private String jobName;

    protected ImmutableSignJobData(String jobName) {
        this.jobName = jobName;
        registerGetters();
    }

    public ImmutableValue<String> jobName() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.JOB_NAME, this.jobName, "").asImmutable();
    }

    @Override
    protected void registerGetters() {
        registerFieldGetter(LiteKeys.JOB_NAME, () -> this.jobName);
        registerKeyValue(LiteKeys.JOB_NAME, this::jobName);
    }

    @Override
    public <E> Optional<ImmutableSignJobData> with(Key<? extends BaseValue<E>> key, E value) {
        return Optional.empty();
    }

    @Override
    public int compareTo(ImmutableSignJobData o) {
        return ComparisonChain.start()
                .compare(o.jobName, this.jobName)
                .result();
    }

    @Override
    public int getContentVersion() {
        return 1;
    }

    @Override
    public DataContainer toContainer() {
        return super.toContainer()
                .set(LiteKeys.JOB_NAME, this.jobName);
    }

    @Override
    public SignJobData asMutable() {
        return new SignJobData(jobName);
    }

} @Deprecated
public class JobData extends AbstractData<JobData, ImmutableJobData> implements Comparable<JobData> {

    private int level;
    private int exp;
    private String jobName;

    protected JobData(String jobName, int level, int exp) {
        this.jobName = jobName;
        this.level = level;
        this.exp = exp;
        registerGettersAndSetters();
    }

    public Value<String> jobName() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.JOB_NAME, this.jobName, "");
    }

    public Value<Integer> level() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.LEVEL, this.level, 0);
    }

    public Value<Integer> exp() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.EXP, this.exp, 0);
    }

    @Override
    protected void registerGettersAndSetters() {
        registerFieldGetter(LiteKeys.LEVEL, () -> this.level);
        registerFieldSetter(LiteKeys.LEVEL, value -> this.level = value);
        registerKeyValue(LiteKeys.LEVEL, this::level);

        registerFieldGetter(LiteKeys.EXP, () -> this.exp);
        registerFieldSetter(LiteKeys.EXP, value -> this.exp = value);
        registerKeyValue(LiteKeys.EXP, this::exp);

        registerFieldGetter(LiteKeys.JOB_NAME, () -> this.jobName);
        registerFieldSetter(LiteKeys.JOB_NAME, value -> this.jobName = value);
        registerKeyValue(LiteKeys.JOB_NAME, this::jobName);
    }

    @Override
    public JobData copy() {
        return new JobData(jobName, level, exp);
    }

    @Override
    public Optional<JobData> fill(DataHolder dataHolder, MergeFunction mergeFn) {
        if (!dataHolder.getKeys().contains(LiteKeys.JOB_NAME) || !dataHolder.getKeys().contains(LiteKeys.LEVEL) ||
                !dataHolder.getKeys().contains(LiteKeys.EXP)) {
            return Optional.empty();
        }
        final String jobName = dataHolder.get(LiteKeys.JOB_NAME).get();
        final int exp = dataHolder.get(LiteKeys.EXP).get();
        final int level = dataHolder.get(LiteKeys.LEVEL).get();
        this.jobName = jobName;
        this.exp = exp;
        this.level = level;
        return Optional.of(this);
    }

    @Override
    public Optional<JobData> from(DataContainer container) {
        if (!container.contains(LiteKeys.JOB_NAME.getQuery(), LiteKeys.LEVEL.getQuery(), LiteKeys.EXP.getQuery())) {
            return Optional.empty();
        }
        final String jobName = container.getString(LiteKeys.JOB_NAME.getQuery()).get();
        final int exp = container.getInt(LiteKeys.EXP.getQuery()).get();
        final int level = container.getInt(LiteKeys.LEVEL.getQuery()).get();
        this.jobName = jobName;
        this.exp = exp;
        this.level = level;
        return Optional.of(this);
    }

    @Override
    public int getContentVersion() {
        return 1;
    }

    @Override
    public DataContainer toContainer() {
        return super.toContainer()
                .set(LiteKeys.JOB_NAME, this.jobName)
                .set(LiteKeys.LEVEL, this.level)
                .set(LiteKeys.EXP, this.exp);
    }

    @Override
    public ImmutableJobData asImmutable() {
        return new ImmutableJobData(jobName, level, exp);
    }

    @Override
    public int compareTo(JobData o) {
        return ComparisonChain.start()
                .compare(o.jobName, this.jobName)
                .compare(o.level, this.level)
                .compare(o.exp, this.exp)
                .result();
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("jobName", this.jobName)
                .add("level", this.level)
                .add("exp", this.exp)
                .toString();
    }

} @Deprecated
public class JobDataManipulatorBuilder implements DataManipulatorBuilder<JobData, ImmutableJobData> {

    private int level;
    private int exp;
    private String jobName;

    @Override
    public Optional<JobData> build(DataView container) throws InvalidDataException {
        if (container.contains(LiteKeys.JOB_NAME.getQuery(), LiteKeys.LEVEL.getQuery(), LiteKeys.EXP.getQuery())) {
            final String jobName = container.getString(LiteKeys.JOB_NAME.getQuery()).get();
            final int level = container.getInt(LiteKeys.LEVEL.getQuery()).get();
            final int exp = container.getInt(LiteKeys.EXP.getQuery()).get();
            return Optional.of(new JobData(jobName, level, exp));
        }
        return Optional.empty();
    }

    public JobDataManipulatorBuilder setJobInfo(String jobName, int level, int exp) {
        this.jobName = jobName;
        this.level = level;
        this.exp = exp;
        return this;
    }

    @Override
    public JobData create() {
        if (level < 0) {
            level = 0;
        }
        if (exp < 0) {
            exp = 0;
        }
        if (jobName == null) {
            jobName = "";
        }
        return new JobData(jobName, level, exp);
    }

    @Override
    public Optional<JobData> createFrom(DataHolder dataHolder) {
        return Optional.of(dataHolder.get(JobData.class).orElse(new JobData("", 0, 0)));
    }

} public class SignJobData extends AbstractData<SignJobData, ImmutableSignJobData> implements Comparable<SignJobData> {

    private String jobName;

    protected SignJobData(String jobName) {
        this.jobName = jobName;
        registerGettersAndSetters();
    }

    public Value<String> jobName() {
        return Sponge.getRegistry().getValueFactory().createValue(LiteKeys.JOB_NAME, this.jobName, "");
    }

    @Override
    protected void registerGettersAndSetters() {
        registerFieldGetter(LiteKeys.JOB_NAME, () -> this.jobName);
        registerFieldSetter(LiteKeys.JOB_NAME, value -> this.jobName = value);
        registerKeyValue(LiteKeys.JOB_NAME, this::jobName);
    }

    @Override
    public SignJobData copy() {
        return new SignJobData(jobName);
    }

    @Override
    public Optional<SignJobData> fill(DataHolder dataHolder, MergeFunction mergeFn) {
        if (!dataHolder.getKeys().contains(LiteKeys.JOB_NAME)) {
            return Optional.empty();
        }
        final String jobName = dataHolder.get(LiteKeys.JOB_NAME).get();
        this.jobName = jobName;
        return Optional.of(this);
    }

    @Override
    public Optional<SignJobData> from(DataContainer container) {
        if (!container.contains(LiteKeys.JOB_NAME.getQuery())) {
            return Optional.empty();
        }
        final String jobName = container.getString(LiteKeys.JOB_NAME.getQuery()).get();
        this.jobName = jobName;
        return Optional.of(this);
    }

    @Override
    public int getContentVersion() {
        return 1;
    }

    @Override
    public DataContainer toContainer() {
        return super.toContainer()
                .set(LiteKeys.JOB_NAME, this.jobName);
    }

    @Override
    public ImmutableSignJobData asImmutable() {
        return new ImmutableSignJobData(jobName);
    }

    @Override
    public int compareTo(SignJobData o) {
        return ComparisonChain.start()
                .compare(o.jobName, this.jobName)
                .result();
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("jobName", this.jobName)
                .toString();
    }

} public class SignJobDataManipulatorBuilder implements DataManipulatorBuilder<SignJobData, ImmutableSignJobData> {

    private String jobName;

    @Override
    public Optional<SignJobData> build(DataView container) throws InvalidDataException {
        if (container.contains(LiteKeys.JOB_NAME.getQuery())) {
            final String jobName = container.getString(LiteKeys.JOB_NAME.getQuery()).get();
            return Optional.of(new SignJobData(jobName));
        }
        return Optional.empty();
    }

    public SignJobDataManipulatorBuilder setSignInfo(String jobName) {
        this.jobName = jobName;
        return this;
    }

    @Override
    public SignJobData create() {
        if (jobName == null) {
            jobName = "";
        }
        return new SignJobData(jobName);
    }

    @Override
    public Optional<SignJobData> createFrom(DataHolder dataHolder) {
        return Optional.of(dataHolder.get(SignJobData.class).orElse(new SignJobData("")));
    }

} public class LocalJobManager implements JobManager {

    private FileManager fileManager = JobsLite.getFileManager();

    @Override
    public boolean jobExists(String job) {
        return fileManager.nodeExists("jobsData.conf", job);
    }

    @Override
    public Optional<Job> getJob(String job) {
        if (jobExists(job)) {
            Optional<String> dOpt = fileManager.getValue("jobsData.conf", job + ".displayName", String.class);
            Optional<Integer> mOpt = fileManager.getValue("jobsData.conf", job + ".maxLevel", Integer.class);
            Optional<String> cOpt = fileManager.getValue("jobsData.conf", job + ".color", String.class);
            Optional<Boolean> sOpt = fileManager.getValue("jobsData.conf", job + ".silkTouch", Boolean.class);
            Optional<Boolean> wOpt = fileManager.getValue("jobsData.conf", job + ".worldGen", Boolean.class);
            Optional<Boolean> iOpt = fileManager.getValue("jobsData.conf", job + ".ignoreData", Boolean.class);
            Optional<String> eQOpt = fileManager.getValue("jobsData.conf", job + ".expRequiredProgressionEquation", String.class);
            Optional<String> cQOpt = fileManager.getValue("jobsData.conf", job + ".rewardProgressionEquation", String.class);
            boolean ignoreData = iOpt.isPresent() ? iOpt.get() : false;
            boolean worldGen = wOpt.isPresent() ? wOpt.get() : false;
            boolean silkTouch = sOpt.isPresent() ? sOpt.get() : false;
            if (dOpt.isPresent() && mOpt.isPresent() && cOpt.isPresent() && eQOpt.isPresent() && cQOpt.isPresent()) {
                Map<String, Reward> blockBreaks = new HashMap<>();
                Map<String, Reward> blockPlaces = new HashMap<>();
                Map<String, Reward> mobKills = new HashMap<>();
                Optional<ConfigurationNode> fOpt = fileManager.getFile("jobsData.conf");
                if (fOpt.isPresent()) {
                    // Block breaks
                    fOpt.get().getNode(job).getNode("breaks").getChildrenMap().keySet().forEach(raw -> {
                        String id = raw.toString();
                        Optional<Double> curOpt = fileManager.getValue("jobsData.conf", job + ".breaks." + id + ".currency", Double.class);
                        Optional<Double> expOpt = fileManager.getValue("jobsData.conf", job + ".breaks." + id + ".exp", Double.class);
                        if (curOpt.isPresent() && expOpt.isPresent()) {
                            blockBreaks.put(id, new Reward(curOpt.get(), expOpt.get()));
                        }
                    });
                    // Block places
                    fOpt.get().getNode(job).getNode("places").getChildrenMap().keySet().forEach(raw -> {
                        String id = raw.toString();
                        Optional<Double> curOpt = fileManager.getValue("jobsData.conf", job + ".places." + id + ".currency", Double.class);
                        Optional<Double> expOpt = fileManager.getValue("jobsData.conf", job + ".places." + id + ".exp", Double.class);
                        if (curOpt.isPresent() && expOpt.isPresent()) {
                            blockPlaces.put(id, new Reward(curOpt.get(), expOpt.get()));
                        }
                    });
                    // Mob kills
                    fOpt.get().getNode(job).getNode("kills").getChildrenMap().keySet().forEach(raw -> {
                        String id = raw.toString();
                        Optional<Double> curOpt = fileManager.getValue("jobsData.conf", job + ".kills." + id + ".currency", Double.class);
                        Optional<Double> expOpt = fileManager.getValue("jobsData.conf", job + ".kills." + id + ".exp", Double.class);
                        if (curOpt.isPresent() && expOpt.isPresent()) {
                            mobKills.put(id, new Reward(curOpt.get(), expOpt.get()));
                        }
                    });
                    // Finalize
                    String color = cOpt.get();
                    Optional<TextColor> tOpt = Sponge.getRegistry().getType(TextColor.class, color);
                    if (tOpt.isPresent()) {
                        return Optional.of(new Job(job, dOpt.get(), mOpt.get(), tOpt.get(), silkTouch, worldGen, ignoreData, eQOpt.get(),
                                cQOpt.get(), blockBreaks, blockPlaces, mobKills));
                    }
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public List<Job> getJobs() {
        List<Job> jobs = new ArrayList<>();
        Optional<ConfigurationNode> fOpt = fileManager.getFile("jobsData.conf");
        if (fOpt.isPresent()) {
            fOpt.get().getChildrenMap().keySet().forEach(raw -> {
                Optional<Job> jOpt = getJob(raw.toString());
                if (jOpt.isPresent()) {
                    jobs.add(jOpt.get());
                }
            });
        }
        return jobs;
    }

    @Override
    public boolean createJob(String id, String displayName, int maxLevel, TextColor textColor, boolean silkTouch, boolean worldGen,
            boolean ignoreData, HashMap<BlockState, Reward> blockBreaks, HashMap<BlockState, Reward> blockPlaces,
            HashMap<EntityType, Reward> mobKills) {
        if (!jobExists(id)) {
            fileManager.setValue("jobsData.conf", id + ".displayName", String.class, displayName);
            fileManager.setValue("jobsData.conf", id + ".maxLevel", Integer.class, maxLevel);
            fileManager.setValue("jobsData.conf", id + ".color", String.class, textColor.toString());
            fileManager.setValue("jobsData.conf", id + ".silkTouch", Boolean.class, silkTouch);
            fileManager.setValue("jobsData.conf", id + ".worldGen", Boolean.class, worldGen);
            fileManager.setValue("jobsData.conf", id + ".ignoreData", Boolean.class, ignoreData);
            fileManager.setValue("jobsData.conf", id + ".expRequiredProgressionEquation", String.class, "100*((1.12)^((currentLevel-1)*1.88))");
            fileManager.setValue("jobsData.conf", id + ".rewardProgressionEquation", String.class, "startingPoint*((1.048)^(currentLevel-1))");
            blockBreaks.entrySet().forEach(entry -> {
                String key = entry.getKey().toString();
                Reward reward = entry.getValue();
                fileManager.setValue("jobsData.conf", id + ".breaks." + key + ".currency", Double.class, reward.getCurrency());
                fileManager.setValue("jobsData.conf", id + ".breaks." + key + ".exp", Double.class, reward.getExp());
            });
            blockPlaces.entrySet().forEach(entry -> {
                String key = entry.getKey().toString();
                Reward reward = entry.getValue();
                fileManager.setValue("jobsData.conf", id + ".places." + key + ".currency", Double.class, reward.getCurrency());
                fileManager.setValue("jobsData.conf", id + ".places." + key + ".exp", Double.class, reward.getExp());
            });
            mobKills.entrySet().forEach(entry -> {
                String key = entry.getKey().getId();
                Reward reward = entry.getValue();
                fileManager.setValue("jobsData.conf", id + ".kills." + key + ".currency", Double.class, reward.getCurrency());
                fileManager.setValue("jobsData.conf", id + ".kills." + key + ".exp", Double.class, reward.getExp());
            });
            return true;
        }
        return false;
    }

    @Override
    public boolean deleteJob(String job) {
        boolean del = fileManager.deleteValue("jobsData.conf", job);
        boolean player = JobsLite.getPlayerManager().removeJob(job);
        return del && player;
    }

} public class LocalPlayerManager implements PlayerManager {

    private FileManager fileManager = JobsLite.getFileManager();

    @Override
    public List<String> getCurrentJobs(UUID uuid) {
        List<String> jobs = new ArrayList<>();
        Optional<ConfigurationNode> fOpt = fileManager.getFile("playerjobs.conf");
        if (fOpt.isPresent() && !fOpt.get().getNode(uuid.toString()).isVirtual()) {
            fOpt.get().getNode(uuid.toString()).getChildrenMap().keySet().forEach(raw -> {
                jobs.add(raw.toString());
            });
        }
        return jobs;
    }

    @Override
    public int getCurrentLevel(UUID uuid, String job) {
        if (hasJob(uuid, job)) {
            Optional<Integer> iOpt = fileManager.getValue("playerjobs.conf", uuid.toString() + "." + job + ".level", Integer.class);
            if (iOpt.isPresent()) {
                return iOpt.get();
            }
        }
        return -1;
    }

    @Override
    public Optional<Double> getCurrentExp(UUID uuid, String job) {
        if (hasJob(uuid, job)) {
            return fileManager.getValue("playerjobs.conf", uuid.toString() + "." + job + ".exp", Double.class);
        }
        return Optional.empty();
    }

    @Override
    public boolean setExp(UUID uuid, String job, double exp) {
        return fileManager.setValue("playerjobs.conf", uuid.toString() + "." + job + ".exp", Double.class, exp);
    }

    @Override
    public boolean setLevel(UUID uuid, String job, int level) {
        return fileManager.setValue("playerjobs.conf", uuid.toString() + "." + job + ".level", Integer.class, level);
    }

    @Override
    public boolean addJob(UUID uuid, String job) {
        return fileManager.setValue("playerjobs.conf", uuid.toString() + "." + job + ".exp", Double.class, 0.0)
                && fileManager.setValue("playerjobs.conf", uuid.toString() + "." + job + ".level", Integer.class, 0);
    }

    @Override
    public boolean removeAllJobs(UUID uuid) {
        return fileManager.deleteValue("playerjobs.conf", uuid.toString());
    }

    @Override
    public boolean removeJob(UUID uuid, String job) {
        return fileManager.deleteValue("playerjobs.conf", uuid.toString() + "." + job);
    }

    @Override
    public boolean removeJob(String job) {
        Optional<ConfigurationNode> fOpt = fileManager.getFile("playerjobs.conf");
        if (fOpt.isPresent()) {
            ConfigurationNode node = fOpt.get();
            node.getChildrenMap().keySet().forEach(raw -> {
                if (raw instanceof String) {
                    String uuid = (String) raw;
                    fileManager.deleteValue("playerjobs.conf", uuid + "." + job);
                }
            });
        }
        return false;
    }

} public class RemoteJobManager implements JobManager {

    private SqlManager manager;

    public RemoteJobManager(String hostname, String port, String database, String username, String password) {
        manager = RemoteSqlManager.createInstance(JobsLite.getInstance(), hostname, port, database, username, password).get();
        if (manager.initialTestConnection()) {
            manager.executeUpdate("CREATE TABLE IF NOT EXISTS jobslitejobs(id VARCHAR(1024), name VARCHAR(1024), maxlevel INT(11), color VARCHAR(1024)"
                    + ", silktouch BOOLEAN, worldgen BOOLEAN, ignoredata BOOLEAN, expeq VARCHAR(1024), reweq VARCHAR(1024))");
            manager.executeUpdate("CREATE TABLE IF NOT EXISTS jobslitebreaks(job VARCHAR(1024), type VARCHAR(1024), cur DOUBLE, exp DOUBLE)");
            manager.executeUpdate("CREATE TABLE IF NOT EXISTS jobsliteplaces(job VARCHAR(1024), type VARCHAR(1024), cur DOUBLE, exp DOUBLE)");
            manager.executeUpdate("CREATE TABLE IF NOT EXISTS jobslitekills(job VARCHAR(1024), type VARCHAR(1024), cur DOUBLE, exp DOUBLE)");
        }
    }

    public boolean isWorking() {
        return manager.testConnection();
    }

    @Override
    public boolean jobExists(String job) {
        return manager.queryExists("SELECT id FROM jobslitejobs WHERE id = ?", job);
    }

    @Override
    public Optional<Job> getJob(String job) {
        if (jobExists(job)) {
            Optional<ResultSet> oRs = manager.executeQuery("SELECT * FROM jobslitejobs WHERE id = ?", job);
            Optional<ResultSet> bRs = manager.executeQuery("SELECT * FROM jobslitebreaks WHERE job = ?", job);
            Optional<ResultSet> pRs = manager.executeQuery("SELECT * FROM jobsliteplaces WHERE job = ?", job);
            Optional<ResultSet> kRs = manager.executeQuery("SELECT * FROM jobslitekills WHERE job = ?", job);
            if (oRs.isPresent() && bRs.isPresent() && pRs.isPresent() && kRs.isPresent()) {
                ResultSet rs = oRs.get();
                ResultSet brs = bRs.get();
                ResultSet prs = pRs.get();
                ResultSet krs = kRs.get();
                try {
                    rs.first();
                    String name = rs.getString("name");
                    int maxLevel = rs.getInt("maxlevel");
                    String color = rs.getString("color");
                    boolean silkTouch = rs.getBoolean("silktouch");
                    boolean worldGen = rs.getBoolean("worldgen");
                    boolean ignoreData = rs.getBoolean("ignoredata");
                    String expEq = rs.getString("expeq");
                    String rewEq = rs.getString("reweq");
                    Map<String, Reward> blockBreaks = new HashMap<>();
                    Map<String, Reward> blockPlaces = new HashMap<>();
                    Map<String, Reward> mobKills = new HashMap<>();

                    while (brs.next()) {
                        blockBreaks.put(brs.getString("type"), new Reward(brs.getDouble("cur"), brs.getDouble("exp")));
                    }
                    while (prs.next()) {
                        blockPlaces.put(prs.getString("type"), new Reward(prs.getDouble("cur"), prs.getDouble("exp")));
                    }
                    while (krs.next()) {
                        mobKills.put(krs.getString("type"), new Reward(krs.getDouble("cur"), krs.getDouble("exp")));
                    }

                    // Finalize
                    Optional<TextColor> tOpt = Sponge.getRegistry().getType(TextColor.class, color);
                    if (tOpt.isPresent()) {
                        return Optional.of(new Job(job, name, maxLevel, tOpt.get(), silkTouch, worldGen, ignoreData, expEq, rewEq, blockBreaks,
                                blockPlaces, mobKills));
                    }

                } catch (Exception e) {
                    JobsLite.getInstance().logger.error(e.getMessage());
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public List<Job> getJobs() {
        List<Job> jobs = new ArrayList<>();
        List<String> jobStrings = manager.queryTypeList("id", String.class, "SELECT id FROM jobslitejobs");
        jobStrings.forEach(jobString -> {
            Optional<Job> jOpt = getJob(jobString);
            if (jOpt.isPresent())
                jobs.add(jOpt.get());
        });
        return jobs;
    }

    @Override
    public boolean createJob(String id, String displayName, int maxLevel, TextColor textColor, boolean silkTouch, boolean worldGen,
            boolean ignoreData, HashMap<BlockState, Reward> blockBreaks, HashMap<BlockState, Reward> blockPlaces,
            HashMap<EntityType, Reward> mobKills) {
        if (!jobExists(id)) {
            if (!manager.executeUpdate(
                    "INSERT INTO jobslitejobs (`id`, `name`, `maxlevel`, `color`, `silktouch`, `worldgen`, `ignoredata`, `expeq`, "
                            + "`reweq`) VALUES (?,?,?,?,?,?,?,?,?)", id, displayName, Integer.toString(maxLevel), textColor.toString(),
                    Integer.toString((silkTouch) ? 1 : 0), Integer.toString((worldGen) ? 1 : 0), Integer.toString((ignoreData) ? 1 : 0),
                    "100*((1.12)^((currentLevel-1)*1.88))", "startingPoint*((1.048)^(currentLevel-1))"))
                return false;
            for (Entry<BlockState, Reward> entry : blockBreaks.entrySet()) {
                Reward r = entry.getValue();
                if (!manager.executeUpdate("INSERT INTO jobslitebreaks (`job`, `type`, `cur`, `exp`) VALUES (?,?,?,?)", id, entry.getKey()
                        .toString(), Double.toString(r.getCurrency()), Double.toString(r.getExp())))
                    return false;
            }
            for (Entry<BlockState, Reward> entry : blockPlaces.entrySet()) {
                Reward r = entry.getValue();
                if (!manager.executeUpdate("INSERT INTO jobsliteplaces (`job`, `type`, `cur`, `exp`) VALUES (?,?,?,?)", id, entry.getKey()
                        .toString(), Double.toString(r.getCurrency()), Double.toString(r.getExp())))
                    return false;
            }
            for (Entry<EntityType, Reward> entry : mobKills.entrySet()) {
                Reward r = entry.getValue();
                if (!manager.executeUpdate("INSERT INTO jobslitekills (`job`, `type`, `cur`, `exp`) VALUES (?,?,?,?)", id, entry.getKey()
                        .toString(), Double.toString(r.getCurrency()), Double.toString(r.getExp())))
                    return false;
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean deleteJob(String job) {
        return (manager.executeUpdate("DELETE FROM jobslitejobs WHERE id = ?", job) && manager.executeUpdate(
                "DELETE FROM jobslitebreaks WHERE job = ?", job) && manager.executeUpdate("DELETE FROM jobsliteplaces WHERE job = ?", job)
                && manager.executeUpdate("DELETE FROM jobslitekills WHERE job = ?", job));
    }
} public class RemotePlayerManager implements PlayerManager {

    private SqlManager manager;

    public RemotePlayerManager(String hostname, String port, String database, String username, String password) {
        manager = RemoteSqlManager.createInstance(JobsLite.getInstance(), hostname, port, database, username, password).get();
        if (manager.initialTestConnection())
            manager.executeUpdate("CREATE TABLE IF NOT EXISTS jobsliteplayers(uuid VARCHAR(36), job VARCHAR(1024), exp DECIMAL(11,2), level INT(11))");
    }

    public boolean isWorking() {
        return manager.testConnection();
    }

    @Override
    public List<String> getCurrentJobs(UUID uuid) {
        List<String> jobs = new ArrayList<>();
        Optional<ResultSet> rOpt = manager.executeQuery("SELECT job FROM jobsliteplayers WHERE uuid = ?", uuid.toString());
        if (rOpt.isPresent()) {
            ResultSet rs = rOpt.get();
            try {
                while (rs.next()) {
                    jobs.add(rs.getString("job"));
                }
            } catch (SQLException e) {
                return jobs;
            }
        }
        return jobs;
    }

    @Override
    public int getCurrentLevel(UUID uuid, String job) {
        if (getCurrentJobs(uuid).contains(job)) {
            Optional<Integer> iOpt =
                    manager.queryType("level", Integer.class, "SELECT level FROM jobsliteplayers WHERE uuid = ? and job = ?", uuid.toString(), job);
            if (iOpt.isPresent()) {
                return iOpt.get();
            }
        }
        return 0;
    }

    @Override
    public Optional<Double> getCurrentExp(UUID uuid, String job) {
        if (getCurrentJobs(uuid).contains(job)) {
            Optional<BigDecimal> bOpt =
                    manager.queryType("exp", BigDecimal.class, "SELECT exp FROM jobsliteplayers WHERE uuid = ? and job = ?", uuid.toString(), job);
            if (bOpt.isPresent()) {
                return Optional.of(Double.parseDouble(bOpt.get().toString()));
            }
        }
        return Optional.empty();
    }

    @Override
    public boolean setExp(UUID uuid, String job, double exp) {
        if (getCurrentJobs(uuid).contains(job)) {
            return manager.executeUpdate("UPDATE jobsliteplayers SET exp = ? WHERE uuid = ? AND job = ?", Double.toString(exp), uuid.toString(),
                    job);
        }
        return false;
    }

    @Override
    public boolean setLevel(UUID uuid, String job, int level) {
        if (getCurrentJobs(uuid).contains(job)) {
            return manager.executeUpdate("UPDATE jobsliteplayers SET level = ? WHERE uuid = ? AND job = ?", Integer.toString(level), uuid.toString(),
                    job);
        }
        return false;
    }

    @Override
    public boolean addJob(UUID uuid, String job) {
        if (!getCurrentJobs(uuid).contains(job)) {
            return manager.executeUpdate("INSERT INTO jobsliteplayers (`uuid`, `job`, `exp`, `level`) VALUES (?, ?, ?, ?)", uuid.toString(), job,
                    "0", "0");
        }
        return false;
    }

    @Override
    public boolean removeAllJobs(UUID uuid) {
        return manager.executeUpdate("DELETE FROM jobsliteplayers WHERE uuid = ?", uuid.toString());
    }

    @Override
    public boolean removeJob(UUID uuid, String job) {
        return manager.executeUpdate("DELETE FROM jobsliteplayers WHERE uuid = ? AND job = ?", uuid.toString(), job);
    }

    @Override
    public boolean removeJob(String job) {
        return manager.executeUpdate("DELETE FROM jobsliteplayers WHERE AND job = ?", job);
    }

} public class MobDeathListener {

    private UniqueAccount account;
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Listener
    public void onBlockBreak(DestructEntityEvent.Death event, @First EntityDamageSource source) {
        Sponge.getScheduler().createTaskBuilder().execute(c -> {
            run(event, source);
        }).async().submit(JobsLite.getInstance());
    }

    private void run(DestructEntityEvent.Death event, EntityDamageSource source) {
        if (source.getSource() instanceof Player) {
            Player player = ((Player) source.getSource());
            UUID uuid = player.getUniqueId();
            PlayerManager playerManager = JobsLite.getPlayerManager();
            JobManager jobManager = JobsLite.getJobManager();
            for (String jobString : playerManager.getCurrentJobs(uuid)) {
                Optional<Job> jOpt = jobManager.getJob(jobString);
                if (jOpt.isPresent()) {
                    Job job = jOpt.get();
                    String jobId = job.getId();
                    String jobName = job.getDisplayName();
                    Optional<UniqueAccount> uOpt = JobsLite.getEconomyService().getOrCreateAccount(player.getUniqueId());
                    if (!uOpt.isPresent()) {
                        return;
                    }
                    account = uOpt.get();
                    for (Entry<String, Reward> entry : job.getMobKills().entrySet()) {
                        if (entry.getKey().equalsIgnoreCase(event.getTargetEntity().getType().getId())) {
                            // Mob is a match!
                            // Get all variables
                            // Max Level
                            int maxLevel = job.getMaxLevel();
                            // Current Level
                            int playerLevel = playerManager.getCurrentLevel(uuid, jobId);
                            if (playerLevel < 0)
                                continue;
                            // Current Exp
                            Optional<Double> playerExp = playerManager.getCurrentExp(uuid, jobId);
                            if (!playerExp.isPresent())
                                continue;
                            // Base exp reward
                            double baseExpReward = entry.getValue().getExp();
                            // Base currency reward
                            double baseCurrencyReward = entry.getValue().getCurrency();
                            // Get the equations
                            String rewardEquation = job.getCurEquation();
                            String rewardCurrencyEquation = rewardEquation;
                            String expEquation = job.getExpEquation();
                            // Replace the variables in the equation
                            rewardEquation =
                                    rewardEquation.replaceAll("startingPoint", baseExpReward + "").replaceAll("currentLevel",
                                            playerLevel + "");
                            rewardCurrencyEquation =
                                    rewardCurrencyEquation.replaceAll("startingPoint", baseCurrencyReward + "").replaceAll(
                                            "currentLevel", playerLevel + "");
                            expEquation = expEquation.replaceAll("currentLevel", playerLevel + "");
                            // Calculate the data
                            double reward;
                            double expRequired = NumberUtils.eval(expEquation);
                            double currencyReward;
                            if (playerLevel == 0) {
                                reward = baseExpReward;
                                currencyReward = baseCurrencyReward;
                            } else {
                                reward = NumberUtils.eval(rewardEquation);
                                currencyReward = NumberUtils.eval(rewardCurrencyEquation);
                            }
                            // Figure it out
                            if (playerExp.get() + reward >= expRequired) {
                                // Player is leveling up
                                if (playerLevel == maxLevel) {
                                    // Already at max level
                                    addFunds(currencyReward, player);
                                    continue;
                                } else {
                                    if (playerLevel + 1 == maxLevel) {
                                        playerManager.setLevel(uuid, jobId, playerLevel + 1);
                                        // Sound
                                        player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                        addFunds(currencyReward, player);
                                        player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(jobName))));
                                        // Tell them they are now at the
                                        // max level
                                        player.sendMessage(messageStorage.getMessage("working.maxlevel", "job", jobName));
                                    } else {
                                        double expLeft = reward - (expRequired - playerExp.get());
                                        playerManager.setExp(uuid, jobId, expLeft);
                                        playerManager.setLevel(uuid, jobId, playerLevel + 1);
                                        // Sound
                                        player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                        addFunds(currencyReward, player);
                                        player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(jobName))));
                                        // Tell them their new
                                        // statistics
                                        String newExpEq = expEquation.replaceAll("currentLevel", (playerLevel + 2) + "");
                                        double newExp = NumberUtils.eval(newExpEq);
                                        player.sendMessage(messageStorage.getMessage("working.nextlevel", "exp", NumberFormat
                                                .getNumberInstance(Locale.US).format(newExp - expLeft)));
                                    }
                                }
                            } else {
                                // Player isn't leveling up
                                addFunds(currencyReward, player);
                                // If the player is at the max level
                                // don't give them exp
                                if (maxLevel == playerLevel)
                                    continue;
                                playerManager.setExp(uuid, jobId, playerExp.get() + reward);
                            }
                        }
                    }
                }
            }
        }
    }

    private void addFunds(double amount, Player player) {
        String virt = JobsLite.getOption("virtual-draw");
        if (virt.equalsIgnoreCase("none")) {
            // Generate money
            account.deposit(JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                    Cause.of(EventContext.empty(),(JobsLite.getInstance().container)));
        } else {
            // Transfer money
            Optional<Account> aOpt = JobsLite.getEconomyService().getOrCreateAccount(virt);
            if (aOpt.isPresent()) {
                if (!aOpt.get()
                        .transfer(account, JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                                Cause.of(EventContext.empty(),(JobsLite.getInstance().container))).getResult().equals(ResultType.SUCCESS)) {
                    // Transfer failed
                    if (!JobsLite.msgCache.contains(player.getUniqueId())) {
                        player.sendMessage(messageStorage.getMessage("working.nofunds"));
                        JobsLite.msgCache.add(player.getUniqueId());
                    }
                }
            } else {
                // Failed to find account
                JobsLite.getInstance().logger.error("Failed to find virtual account! Payment will not be given!");
            }
        }
    }

} public class PlayerBlockBreakListener {

    private UniqueAccount account;
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Listener
    public void onBlockBreak(ChangeBlockEvent.Break event, @First Player player) {
        Sponge.getScheduler().createTaskBuilder().execute(c -> {
            run(event, player);
        }).async().submit(JobsLite.getInstance());
    }

    private void run(ChangeBlockEvent.Break event, Player player) {
        UUID uuid = player.getUniqueId();
        // Load managers
        PlayerManager playerManager = JobsLite.getPlayerManager();
        JobManager jobManager = JobsLite.getJobManager();
        for (String jobString : playerManager.getCurrentJobs(uuid)) {
            Optional<Job> jOpt = jobManager.getJob(jobString);
            if (jOpt.isPresent()) {
                Job job = jOpt.get();
                String displayName = job.getDisplayName();
                String id = job.getId();
                Optional<UniqueAccount> uOpt = JobsLite.getEconomyService().getOrCreateAccount(uuid);
                if (!uOpt.isPresent()) {
                    return;
                }
                if (job.isSilkTouch()) {
                    Optional<ItemStack> iOpt = player.getItemInHand(HandTypes.MAIN_HAND);
                    if (iOpt.isPresent()) {
                        ItemStack item = iOpt.get();
                        EnchantmentData data = item.getOrCreate(EnchantmentData.class).get();
                        for (Enchantment enchantment : data.enchantments()) {
                            if (enchantment.getType().equals(EnchantmentTypes.SILK_TOUCH)) {
                                return;
                            }
                        }
                    }
                }
                account = uOpt.get();
                for (Entry<String, Reward> entry : job.getBlockBreaks().entrySet()) {
                    for (Transaction<BlockSnapshot> transaction : event.getTransactions()) {
                        if (transaction.isValid()) {
                            BlockState blockTransactionState = transaction.getOriginal().getState();
                            if (job.isWorldGen()) {
                                Optional<UUID> uuOpt = transaction.getOriginal().getCreator();
                                if (uuOpt.isPresent()) {
                                    continue;
                                }
                            }
                            String block = entry.getKey();
                            boolean pass = false;
                            if (job.isIgnoreData()) {
                                // Ignore data
                                String newBlock = block.replaceAll("\\[.*?]", "");
                                if (newBlock.equalsIgnoreCase(blockTransactionState.getId().replaceAll("\\[.*?]", ""))) {
                                    pass = true;
                                }
                            } else {
                                // Use data
                                if (block.equalsIgnoreCase(blockTransactionState.toString())) {
                                    pass = true;
                                }
                            }
                            if (pass) {
                                // Block is a match!
                                // Get all variables
                                // Max Level
                                int maxLevel = job.getMaxLevel();
                                // Current Level
                                int playerLevel = playerManager.getCurrentLevel(uuid, id);
                                if (playerLevel < 0)
                                    continue;
                                // Current Exp
                                Optional<Double> playerExp = playerManager.getCurrentExp(uuid, id);
                                if (!playerExp.isPresent())
                                    continue;
                                // Base exp reward
                                double baseExpReward = entry.getValue().getExp();
                                // Base currency reward
                                double baseCurrencyReward = entry.getValue().getCurrency();
                                // Get the equations
                                String rewardEquation = job.getCurEquation();
                                String rewardCurrencyEquation = rewardEquation;
                                String expEquation = job.getExpEquation();
                                // Replace the variables in the equation
                                rewardEquation =
                                        rewardEquation.replaceAll("startingPoint", baseExpReward + "").replaceAll("currentLevel",
                                                playerLevel + "");
                                rewardCurrencyEquation =
                                        rewardCurrencyEquation.replaceAll("startingPoint", baseCurrencyReward + "").replaceAll(
                                                "currentLevel", playerLevel + "");
                                expEquation = expEquation.replaceAll("currentLevel", playerLevel + "");
                                // Calculate the data
                                double reward;
                                double expRequired = NumberUtils.eval(expEquation);
                                double currencyReward;
                                if (playerLevel == 0) {
                                    reward = baseExpReward;
                                    currencyReward = baseCurrencyReward;
                                } else {
                                    reward = NumberUtils.eval(rewardEquation);
                                    currencyReward = NumberUtils.eval(rewardCurrencyEquation);
                                }
                                // Figure it out
                                if (playerExp.get() + reward >= expRequired) {
                                    // Player is leveling up
                                    if (playerLevel == maxLevel) {
                                        // Already at max level
                                        addFunds(currencyReward, player);
                                        continue;
                                    } else {
                                        if (playerLevel + 1 == maxLevel) {
                                            playerManager.setLevel(uuid, id, playerLevel + 1);
                                            // Sound
                                            player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                            addFunds(currencyReward, player);
                                            player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                    Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(displayName))));
                                            // Tell them they are now at the
                                            // max level
                                            player.sendMessage(messageStorage.getMessage("working.maxlevel", "job", displayName));
                                        } else {
                                            double expLeft = reward - (expRequired - playerExp.get());
                                            playerManager.setExp(uuid, id, expLeft);
                                            playerManager.setLevel(uuid, id, playerLevel + 1);
                                            // Sound
                                            player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                            addFunds(currencyReward, player);
                                            player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                    Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(displayName))));
                                            // Tell them their new
                                            // statistics
                                            String newExpEq = expEquation.replaceAll("currentLevel", (playerLevel + 2) + "");
                                            double newExp = NumberUtils.eval(newExpEq);
                                            player.sendMessage(messageStorage.getMessage("working.nextlevel", "exp", NumberFormat
                                                    .getNumberInstance(Locale.US).format(newExp - expLeft)));
                                        }
                                    }
                                } else {
                                    // Player isn't leveling up
                                    addFunds(currencyReward, player);
                                    // If the player is at the max level
                                    // don't give them exp
                                    if (maxLevel == playerLevel)
                                        continue;
                                    playerManager.setExp(uuid, id, playerExp.get() + reward);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private void addFunds(double amount, Player player) {
        String virt = JobsLite.getOption("virtual-draw");
        if (virt.equalsIgnoreCase("none")) {
            // Generate money
            account.deposit(JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                    Cause.of(EventContext.empty(), JobsLite.getInstance().container));
        } else {
            // Transfer money
            Optional<Account> aOpt = JobsLite.getEconomyService().getOrCreateAccount(virt);
            if (aOpt.isPresent()) {
                if (!aOpt.get()
                        .transfer(account, JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                                Cause.of(EventContext.empty(), JobsLite.getInstance().container)).getResult().equals(ResultType.SUCCESS)) {
                    // Transfer failed
                    if (!JobsLite.msgCache.contains(player.getUniqueId())) {
                        player.sendMessage(messageStorage.getMessage("working.nofunds"));
                        JobsLite.msgCache.add(player.getUniqueId());
                    }
                }
            } else {
                // Failed to find account
                JobsLite.getInstance().logger.error("Failed to find virtual account! Payment will not be given!");
            }
        }
    }

} public class PlayerJoinListener {

    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private JobManager jobManager = JobsLite.getJobManager();

    @Listener
    public void onPlayerJoin(ClientConnectionEvent.Join event) {
        Player player = event.getTargetEntity();
        UUID uuid = player.getUniqueId();
        // Move very old data
        moveData(player);
        // Move slightly old data
        convert(uuid);
        playerManager.getCurrentJobs(uuid).forEach(job -> {
            if (!jobManager.jobExists(job)) {
                playerManager.removeJob(uuid, job);
            }
        });
        JobsLite.getEconomyService().getOrCreateAccount(player.getUniqueId());
    }

    private void moveData(Player player) {
        if (player.get(LiteKeys.JOB_NAME).isPresent()) {
            String jobName = player.get(LiteKeys.JOB_NAME).get();
            if (jobManager.jobExists(jobName)) {
                UUID uuid = player.getUniqueId();
                // Transfer the job
                playerManager.addJob(uuid, jobName);
                playerManager.setExp(uuid, jobName, (double) player.get(LiteKeys.EXP).get());
                playerManager.setLevel(uuid, jobName, player.get(LiteKeys.LEVEL).get());
                // Remove the data
                // player.remove(JobData.class);
            } else {
                // Remove the data
                // player.remove(JobData.class);
            }
        }
    }

    private void convert(UUID uuid) {
        FileManager fileManager = JobsLite.getFileManager();
        Optional<ConfigurationNode> oOpt = fileManager.getFile("playerjobdata.conf");
        Optional<ConfigurationNode> nOpt = fileManager.getFile("playerjobs.conf");
        if (oOpt.isPresent() && nOpt.isPresent()) {
            ConfigurationNode old = oOpt.get();
            old.getChildrenMap().keySet().forEach(raw -> {
                String data = raw.toString();
                Optional<Double> eOpt = fileManager.getValue("playerjobdata.conf", data + ".exp", Double.class);
                Optional<Integer> lOpt = fileManager.getValue("playerjobdata.conf", data + ".level", Integer.class);
                Optional<String> jOpt = fileManager.getValue("playerjobdata.conf", data + ".job", String.class);
                if (eOpt.isPresent() && lOpt.isPresent() && jOpt.isPresent()) {
                    playerManager.addJob(uuid, jOpt.get());
                    playerManager.setExp(uuid, jOpt.get(), eOpt.get());
                    playerManager.setLevel(uuid, jOpt.get(), lOpt.get());
                }
                fileManager.deleteValue("playerjobdata.conf", data);
            });
        }
    }

} public class PlayerPlaceBlockListener {

    private UniqueAccount account;
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Listener
    public void onBlockPlace(ChangeBlockEvent.Place event, @First Player player) {
        Sponge.getScheduler().createTaskBuilder().execute(c -> {
            run(event, player);
        }).async().submit(JobsLite.getInstance());
    }

    private void run(ChangeBlockEvent.Place event, Player player) {
        UUID uuid = player.getUniqueId();
        // Load managers
        PlayerManager playerManager = JobsLite.getPlayerManager();
        JobManager jobManager = JobsLite.getJobManager();
        for (String jobString : playerManager.getCurrentJobs(uuid)) {
            Optional<Job> jOpt = jobManager.getJob(jobString);
            if (jOpt.isPresent()) {
                Job job = jOpt.get();
                String displayName = job.getDisplayName();
                String id = job.getId();
                Optional<UniqueAccount> uOpt = JobsLite.getEconomyService().getOrCreateAccount(player.getUniqueId());
                if (!uOpt.isPresent()) {
                    return;
                }
                account = uOpt.get();
                for (Entry<String, Reward> entry : job.getBlockPlaces().entrySet()) {
                    String block = entry.getKey();
                    for (Transaction<BlockSnapshot> transaction : event.getTransactions()) {
                        if (transaction.isValid()) {
                            BlockState blockTransactionState = transaction.getFinal().getState();
                            boolean pass = false;
                            if (job.isIgnoreData()) {
                                // Ignore data
                                String newBlock = block.replaceAll("\\[.*?]", "");
                                if (newBlock.equalsIgnoreCase(blockTransactionState.getId().replaceAll("\\[.*?]", ""))) {
                                    pass = true;
                                }
                            } else {
                                // Use data
                                if (block.equalsIgnoreCase(blockTransactionState.toString())) {
                                    pass = true;
                                }
                            }
                            if (pass) {
                                // Block is a match!
                                // Get all variables
                                // Max Level
                                int maxLevel = job.getMaxLevel();
                                // Current Level
                                int playerLevel = playerManager.getCurrentLevel(uuid, id);
                                if (playerLevel < 0)
                                    return;
                                // Current Exp
                                Optional<Double> playerExp = playerManager.getCurrentExp(uuid, id);
                                if (!playerExp.isPresent())
                                    return;
                                // Base exp reward
                                double baseExpReward = entry.getValue().getExp();
                                // Base currency reward
                                double baseCurrencyReward = entry.getValue().getCurrency();
                                // Get the equations
                                String rewardEquation = job.getCurEquation();
                                String rewardCurrencyEquation = rewardEquation;
                                String expEquation = job.getExpEquation();
                                // Replace the variables in the equation
                                rewardEquation =
                                        rewardEquation.replaceAll("startingPoint", baseExpReward + "").replaceAll("currentLevel",
                                                playerLevel + "");
                                rewardCurrencyEquation =
                                        rewardCurrencyEquation.replaceAll("startingPoint", baseCurrencyReward + "").replaceAll(
                                                "currentLevel", playerLevel + "");
                                expEquation = expEquation.replaceAll("currentLevel", playerLevel + "");
                                // Calculate the data
                                double reward;
                                double expRequired = NumberUtils.eval(expEquation);
                                double currencyReward;
                                if (playerLevel == 0) {
                                    reward = baseExpReward;
                                    currencyReward = baseCurrencyReward;
                                } else {
                                    reward = NumberUtils.eval(rewardEquation);
                                    currencyReward = NumberUtils.eval(rewardCurrencyEquation);
                                }
                                // Figure it out
                                if (playerExp.get() + reward >= expRequired) {
                                    // Player is leveling up
                                    if (playerLevel == maxLevel) {
                                        // Already at max level
                                        addFunds(currencyReward, player);
                                        return;
                                    } else {
                                        if (playerLevel + 1 == maxLevel) {
                                            playerManager.setLevel(uuid, id, playerLevel + 1);
                                            // Sound
                                            player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                            addFunds(currencyReward, player);
                                            player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                    Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(displayName))));
                                            // Tell them they are now at the
                                            // max level
                                            player.sendMessage(messageStorage.getMessage("working.maxlevel", "job", displayName));
                                        } else {
                                            double expLeft = reward - (expRequired - playerExp.get());
                                            playerManager.setExp(uuid, id, expLeft);
                                            playerManager.setLevel(uuid, id, playerLevel + 1);
                                            // Sound
                                            player.playSound(SoundTypes.ENTITY_PLAYER_LEVELUP, player.getLocation().getPosition(), 1);
                                            addFunds(currencyReward, player);
                                            player.sendMessage(messageStorage.getMessage("working.levelup", ImmutableMap.of("player",
                                                    Text.of(player.getName()), "level", Text.of(playerLevel + 1), "job", Text.of(displayName))));
                                            // Tell them their new
                                            // statistics
                                            String newExpEq = expEquation.replaceAll("currentLevel", (playerLevel + 2) + "");
                                            double newExp = NumberUtils.eval(newExpEq);
                                            player.sendMessage(messageStorage.getMessage("working.nextlevel", "exp", NumberFormat
                                                    .getNumberInstance(Locale.US).format(newExp - expLeft)));
                                        }
                                    }
                                } else {
                                    // Player isn't leveling up
                                    addFunds(currencyReward, player);
                                    // If the player is at the max level
                                    // don't give them exp
                                    if (maxLevel == playerLevel)
                                        return;
                                    playerManager.setExp(uuid, id, playerExp.get() + reward);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private void addFunds(double amount, Player player) {
        String virt = JobsLite.getOption("virtual-draw");
        if (virt.equalsIgnoreCase("none")) {
            // Generate money
            account.deposit(JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                    Cause.of(EventContext.empty(), JobsLite.getInstance().container));
        } else {
            // Transfer money
            Optional<Account> aOpt = JobsLite.getEconomyService().getOrCreateAccount(virt);
            if (aOpt.isPresent()) {
                if (!aOpt.get()
                        .transfer(account, JobsLite.getEconomyService().getDefaultCurrency(), BigDecimal.valueOf(amount),
                                Cause.of(EventContext.empty(),JobsLite.getInstance().container)).getResult().equals(ResultType.SUCCESS)) {
                    // Transfer failed
                    if (!JobsLite.msgCache.contains(player.getUniqueId())) {
                        player.sendMessage(messageStorage.getMessage("working.nofunds"));
                        JobsLite.msgCache.add(player.getUniqueId());
                    }
                }
            } else {
                // Failed to find account
                JobsLite.getInstance().logger.error("Failed to find virtual account! Payment will not be given!");
            }
        }
    }

} public class SignListeners {

    private JobManager jobManager = JobsLite.getJobManager();
    private PlayerManager playerManager = JobsLite.getPlayerManager();
    private MessageStorage messageStorage = JobsLite.getMessageStorage();

    @Listener
    public void onSignChange(ChangeSignEvent event, @First Player player) {
        Sign signTile = event.getTargetTile();
        if (!signTile.get(LiteKeys.JOB_NAME).isPresent() && player.hasPermission("jobs.admin.sign.create")) {
            SignData data = event.getText();
            try {
                if (data.getValue(Keys.SIGN_LINES).get().get(0).toPlain().equals("[JobsLite]")) {
                    String signJob = data.getValue(Keys.SIGN_LINES).get().get(1).toPlain();
                    if (jobManager.jobExists(signJob)) {
                        signTile.offer(data.set(data.getValue(Keys.SIGN_LINES).get().set(0, Text.of("[Job]"))));
                        SignJobDataManipulatorBuilder builder =
                                (SignJobDataManipulatorBuilder) Sponge.getDataManager().getManipulatorBuilder(SignJobData.class).get();
                        SignJobData signJobData = builder.setSignInfo(signJob).create();
                        signTile.offer(signJobData);
                        player.sendMessage(messageStorage.getMessage("sign.success"));
                        return;
                    } else {
                        player.sendMessage(messageStorage.getMessage("sign.nojob"));
                        return;
                    }
                }
            } catch (IndexOutOfBoundsException | NoSuchElementException e) {
                player.sendMessage(messageStorage.getMessage("sign.invalid"));
                return;
            }
        } else if (signTile.get(LiteKeys.JOB_NAME).isPresent() && !player.hasPermission("jobs.admin.sign.delete")) {
            player.sendMessage(messageStorage.getMessage("sign.nopermission"));
            event.setCancelled(true);
        }
    }

    @Listener
    public void onSignClick(InteractBlockEvent.Secondary event, @First Player player) {
        Optional<String> dOpt = event.getTargetBlock().get(LiteKeys.JOB_NAME);
        if (dOpt.isPresent()) {
            String jobString = dOpt.get();
            Optional<Job> jOpt = jobManager.getJob(jobString);
            if (jOpt.isPresent()) {
                Job job = jOpt.get();
                String displayName = job.getDisplayName();
                if (!displayName.isEmpty()) {
                    if (playerManager.getCurrentJobs(player.getUniqueId()).contains(job.getId())) {
                        player.sendMessage(messageStorage.getMessage("command.join.already", "job", displayName));
                        return;
                    }
                    player.sendMessage(messageStorage.getMessage("command.join.confirm", "job", displayName));
                    player.sendMessage(TextUtils.yesOption(new Consumer<CommandSource>() {

                        @Override
                        public void accept(CommandSource source) {
                            if (!playerManager.addJob(player.getUniqueId(), job.getId())) {
                                player.sendMessage(messageStorage.getMessage("generic.error"));
                                return;
                            }
                            player.sendMessage(messageStorage.getMessage("command.join.success", "job", displayName));
                        }

                    }));
                }
            } else {
                player.sendMessage(messageStorage.getMessage("sign.invalid"));
                event.setCancelled(true);
            }
        }
    }

    @Listener
    public void onBlockBreak(ChangeBlockEvent.Break event) {
        Optional<Player> playerOptional = event.getCause().first(Player.class);
        for (Transaction<BlockSnapshot> transaction : event.getTransactions()) {
            if (transaction.isValid()) {
                if (transaction.getOriginal().get(LiteKeys.JOB_NAME).isPresent()) {
                    if (!playerOptional.isPresent()) {
                        event.setCancelled(true);
                        return;
                    }
                    Player player = playerOptional.get();
                    if (!player.hasPermission("jobs.admin.sign.delete")) {
                        player.sendMessage(messageStorage.getMessage("sign.nopermission"));
                        event.setCancelled(true);
                    }
                }
            }
        }
    }
}
import static me.flibio.jobslite.PluginInfo.DESCRIPTION;
import static me.flibio.jobslite.PluginInfo.ID;
import static me.flibio.jobslite.PluginInfo.NAME;
import static me.flibio.jobslite.PluginInfo.VERSION;
import com.google.inject.Inject;
import io.github.flibio.utils.commands.CommandLoader;
import io.github.flibio.utils.file.FileManager;
import io.github.flibio.utils.message.MessageStorage;
import me.flibio.jobslite.api.JobManager;
import me.flibio.jobslite.api.PlayerManager;
import me.flibio.jobslite.bstats.Metrics;
import me.flibio.jobslite.commands.AddCommand;
import me.flibio.jobslite.commands.CreateCommand;
import me.flibio.jobslite.commands.DeleteCommand;
import me.flibio.jobslite.commands.InfoCommand;
import me.flibio.jobslite.commands.JobsCommand;
import me.flibio.jobslite.commands.JoinCommand;
import me.flibio.jobslite.commands.LeaveCommand;
import me.flibio.jobslite.commands.RemoveCommand;
import me.flibio.jobslite.data.ImmutableJobData;
import me.flibio.jobslite.data.ImmutableSignJobData;
import me.flibio.jobslite.data.JobData;
import me.flibio.jobslite.data.JobDataManipulatorBuilder;
import me.flibio.jobslite.data.SignJobData;
import me.flibio.jobslite.data.SignJobDataManipulatorBuilder;
import me.flibio.jobslite.impl.LocalJobManager;
import me.flibio.jobslite.impl.LocalPlayerManager;
import me.flibio.jobslite.impl.RemoteJobManager;
import me.flibio.jobslite.impl.RemotePlayerManager;
import me.flibio.jobslite.listeners.MobDeathListener;
import me.flibio.jobslite.listeners.PlayerBlockBreakListener;
import me.flibio.jobslite.listeners.PlayerJoinListener;
import me.flibio.jobslite.listeners.PlayerPlaceBlockListener;
import me.flibio.jobslite.listeners.SignListeners;
import org.slf4j.Logger;
import org.spongepowered.api.Game;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.GameReloadEvent;
import org.spongepowered.api.event.game.state.GameInitializationEvent;
import org.spongepowered.api.event.game.state.GamePostInitializationEvent;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.event.game.state.GameStartedServerEvent;
import org.spongepowered.api.event.service.ChangeServiceProviderEvent;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.service.economy.EconomyService;
import org.spongepowered.api.text.serializer.TextSerializers;
import java.io.File;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/*
 * This file is part of JobsLite, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015 - 2018 Flibio
 * Copyright (c) Contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package me.flibio.jobslite;




@SuppressWarnings("deprecation")
@Plugin(id = ID, name = NAME, version = VERSION, dependencies = {}, description = DESCRIPTION)
public class JobsLite {

    private static JobsLite instance;

    @Inject @ConfigDir(sharedRoot = false) Path configDir;

    @SuppressWarnings("unused") @Inject private Metrics metrics;

    @Inject public Logger logger;

    @Inject public Game game;

    @Inject public PluginContainer container;

    public String version = JobsLite.class.getAnnotation(Plugin.class).version();

    private static FileManager fileManager;
    private static JobManager jobManager;
    private static PlayerManager playerManager;
    private static EconomyService economyService;
    private static MessageStorage messageStorage;
    public static List<UUID> msgCache = new ArrayList<>();

    private boolean foundProvider = false;
    private boolean late = false;

    @Listener
    public void onPreInitialize(GamePreInitializationEvent event) {
        instance = this;
        // Register the custom data
        Sponge.getDataManager().registerBuilder(JobData.class, new JobDataManipulatorBuilder());
        Sponge.getDataManager().registerBuilder(SignJobData.class, new SignJobDataManipulatorBuilder());
        // Initialze basic plugin managers needed for further initialization
        if (new File(configDir.toString()).isAbsolute()) {
            fileManager = FileManager.createInstance(this, configDir.toString());
        } else {
            fileManager = FileManager.createInstance(this, "./" + configDir.toString());
        }

        messageStorage = MessageStorage.createInstance(this, configDir.toString());
        messageStorage.defaultMessages("jobslitemessages");
    }

    @Listener
    public void onServerInitialize(GameInitializationEvent event) {
        logger.info("JobsLite " + version + " by Flibio initializing!");
        initializeFiles();
        // Initialize the player and job managers
        boolean flatfile = true;
        if (optionEnabled("mysql.enabled")) {
            // Load the remote managers
            RemotePlayerManager tempPlayerManager =
                    new RemotePlayerManager(getOption("mysql.hostname"), getOption("mysql.port"), getOption("mysql.database"),
                            getOption("mysql.username"), getOption("mysql.password"));
            RemoteJobManager tempJobManager =
                    new RemoteJobManager(getOption("mysql.hostname"), getOption("mysql.port"), getOption("mysql.database"),
                            getOption("mysql.username"), getOption("mysql.password"));
            if (tempPlayerManager.isWorking() && tempJobManager.isWorking()) {
                playerManager = tempPlayerManager;
                jobManager = tempJobManager;
                flatfile = false;
            }
        }
        if (flatfile) {
            // Load the local managers
            playerManager = new LocalPlayerManager();
            jobManager = new LocalJobManager();
        }
    }

    @Listener
    public void onChangeServiceProvider(ChangeServiceProviderEvent event) {
        if (event.getService().equals(EconomyService.class) && !late) {
            Object raw = event.getNewProviderRegistration().getProvider();
            if (raw instanceof EconomyService) {
                foundProvider = true;
                economyService = (EconomyService) raw;
            } else {
                foundProvider = false;
            }
        }
    }

    @Listener
    public void onPostInitialization(GamePostInitializationEvent event) {
        if (foundProvider) {
            // Register events and commands
            registerEvents();
            registerCommands();
        }
    }

    @Listener
    public void onServerStart(GameStartedServerEvent event) {
        late = true;
        if (!foundProvider) {
            logger.error("JobsLite failed to load an economy plugin!");
            logger.error("It will no longer function!");
            return;
        }
    }

    @Listener
    public void reload(GameReloadEvent event) {
        // Force a reload of the configuration file
        fileManager.reloadFile("config.conf");
        messageStorage.reloadMessages();
    }

    private void registerEvents() {
        game.getEventManager().registerListeners(this, new PlayerJoinListener());
        game.getEventManager().registerListeners(this, new PlayerBlockBreakListener());
        game.getEventManager().registerListeners(this, new PlayerPlaceBlockListener());
        game.getEventManager().registerListeners(this, new MobDeathListener());
        game.getEventManager().registerListeners(this, new SignListeners());
    }

    private void initializeFiles() {
        fileManager.setDefault("config.conf", "max-jobs", Integer.class, 1);
        fileManager.setDefault("config.conf", "virtual-draw", String.class, "none");
        fileManager.setDefault("config.conf", "mysql.enabled", Boolean.class, false);
        fileManager.setDefault("config.conf", "mysql.hostname", String.class, "host");
        fileManager.setDefault("config.conf", "mysql.port", Integer.class, 3306);
        fileManager.setDefault("config.conf", "mysql.database", String.class, "database");
        fileManager.setDefault("config.conf", "mysql.username", String.class, "username");
        fileManager.setDefault("config.conf", "mysql.password", String.class, "password");
    }

    private void registerCommands() {
        CommandLoader.registerCommands(this, TextSerializers.FORMATTING_CODE.serialize(messageStorage.getMessage("command.invalidsource")),
                new JobsCommand(),