<commit_before>/*
 * This file is part of GriefPrevention, licensed under the MIT License (MIT).
 *
 * Copyright (c) <NAME>
 * 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.ryanhamshire.griefprevention;

import com.flowpowered.math.vector.Vector3d;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import me.ryanhamshire.griefprevention.claim.Claim;
import me.ryanhamshire.griefprevention.claim.ClaimsMode;
import me.ryanhamshire.griefprevention.claim.CreateClaimResult;
import me.ryanhamshire.griefprevention.configuration.ClaimTemplateStorage;
import me.ryanhamshire.griefprevention.configuration.GriefPreventionConfig;
import me.ryanhamshire.griefprevention.configuration.SubDivisionDataConfig;
import me.ryanhamshire.griefprevention.configuration.types.DimensionConfig;
import me.ryanhamshire.griefprevention.configuration.types.GlobalConfig;
import me.ryanhamshire.griefprevention.configuration.types.WorldConfig;
import me.ryanhamshire.griefprevention.event.ClaimDeletedEvent;
import me.ryanhamshire.griefprevention.task.SecureClaimTask;
import me.ryanhamshire.griefprevention.task.SiegeCheckupTask;
import net.minecraft.item.ItemStack;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
import org.apache.commons.lang3.StringUtils;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.entity.living.player.User;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.service.context.Context;
import org.spongepowered.api.service.user.UserStorageService;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.action.TextActions;
import org.spongepowered.api.text.format.TextColor;
import org.spongepowered.api.text.format.TextColors;
import org.spongepowered.api.util.Tristate;
import org.spongepowered.api.world.Chunk;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.storage.WorldProperties;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

//singleton class which manages all GriefPrevention data (except for config options)
public abstract class DataStore {

    // World UUID -> PlayerDataWorldManager
    protected final Map<UUID, ClaimWorldManager> claimWorldManagers = Maps.newHashMap();
    // Global PlayerData manager
    protected ClaimWorldManager globalClaimWorldManager;

    // in-memory cache for group (permission-based) data
    protected ConcurrentHashMap<String, Integer> permissionToBonusBlocksMap = new ConcurrentHashMap<>();

    // in-memory cache for claim data
    public static Map<UUID, GriefPreventionConfig<DimensionConfig>> dimensionConfigMap = Maps.newHashMap();
    public static Map<UUID, GriefPreventionConfig<WorldConfig>> worldConfigMap = Maps.newHashMap();
    public static TreeMap<String, ClaimTemplateStorage> globalTemplates = new TreeMap<>();
    public static GriefPreventionConfig<GlobalConfig> globalConfig;

    // in-memory cache for messages
    protected EnumMap<Messages, CustomizableMessage> messages = new EnumMap<>(Messages.class);

    // pattern for unique user identifiers (UUIDs)
    protected final static Pattern uuidpattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");

    // path information, for where stuff stored on disk is well... stored
    protected final static Path dataLayerFolderPath = Paths.get("config").resolve("GriefPrevention");
    final static Path globalDataPath = Paths.get("GriefPreventionData").resolve("GlobalPlayerData");
    final static Path messagesFilePath = dataLayerFolderPath.resolve("messages.conf");
    final static Path softMuteFilePath = dataLayerFolderPath.resolve("softMute.txt");
    final static Path bannedWordsFilePath = dataLayerFolderPath.resolve("bannedWords.txt");
    public Path globalPlayerDataPath;

    // the latest version of the data schema implemented here
    protected static final int latestSchemaVersion = 2;

    // reading and writing the schema version to the data store
    abstract int getSchemaVersionFromStorage();

    abstract void updateSchemaVersionInStorage(int versionToSet);

    // current version of the schema of data in secondary storage
    private int currentSchemaVersion = -1; // -1 means not determined yet

    // video links
    public static final String SURVIVAL_VIDEO_URL_RAW = "http://bit.ly/mcgpuser";
    static final String CREATIVE_VIDEO_URL_RAW = "http://bit.ly/mcgpcrea";
    static final String SUBDIVISION_VIDEO_URL_RAW = "http://bit.ly/mcgpsub";

    public static boolean generateMessages = true;

    // list of UUIDs which are soft-muted
    ConcurrentHashMap<UUID, Boolean> softMuteMap = new ConcurrentHashMap<UUID, Boolean>();

    protected int getSchemaVersion() {
        if (this.currentSchemaVersion >= 0) {
            return this.currentSchemaVersion;
        } else {
            this.currentSchemaVersion = this.getSchemaVersionFromStorage();
            return this.currentSchemaVersion;
        }
    }

    protected void setSchemaVersion(int versionToSet) {
        this.currentSchemaVersion = versionToSet;
        this.updateSchemaVersionInStorage(versionToSet);
    }

    // initialization!
    void initialize() throws Exception {
        // ensure global player data folder exists
        if (GriefPrevention.getGlobalConfig().getConfig().playerdata.useGlobalPlayerDataStorage) {
            this.globalClaimWorldManager = new ClaimWorldManager();
        }

        this.globalPlayerDataPath = Sponge.getGame().getSavesDirectory().resolve(Sponge.getServer().getDefaultWorldName()).resolve(globalDataPath);
        File globalPlayerDataFolder = this.globalPlayerDataPath.toFile();
        if (!globalPlayerDataFolder.exists()) {
            globalPlayerDataFolder.mkdirs();
        }

        // load up all the messages from messages.hocon
        this.loadMessages();
        GriefPrevention.addLogEntry("Customizable messages loaded.");

        // load list of soft mutes
        this.loadSoftMutes();

        // make a note of the data store schema version
        this.setSchemaVersion(latestSchemaVersion);
    }

    private void loadSoftMutes() {
        File softMuteFile = softMuteFilePath.toFile();
        if (softMuteFile.exists()) {
            BufferedReader inStream = null;
            try {
                // open the file
                inStream = new BufferedReader(new FileReader(softMuteFile.getAbsolutePath()));

                // while there are lines left
                String nextID = inStream.readLine();
                while (nextID != null) {
                    // parse line into a UUID
                    UUID playerID;
                    try {
                        playerID = UUID.fromString(nextID);
                    } catch (Exception e) {
                        playerID = null;
                        GriefPrevention.addLogEntry("Failed to parse soft mute entry as a UUID: " + nextID);
                    }

                    // push it into the map
                    if (playerID != null) {
                        this.softMuteMap.put(playerID, true);
                    }

                    // move to the next
                    nextID = inStream.readLine();
                }
            } catch (Exception e) {
                GriefPrevention.addLogEntry("Failed to read from the soft mute data file: " + e.toString());
                e.printStackTrace();
            }

            try {
                if (inStream != null) {
                    inStream.close();
                }
            } catch (IOException exception) {
            }
        }
    }

    public List<String> loadBannedWords() {
        try {
            File bannedWordsFile = bannedWordsFilePath.toFile();
            if (!bannedWordsFile.exists()) {
                Files.touch(bannedWordsFile);
                String defaultWords =
                        "nigger\nniggers\nniger\nnigga\nnigers\nniggas\n" +
                                "fag\nfags\nfaggot\nfaggots\nfeggit\nfeggits\nfaggit\nfaggits\n" +
                                "cunt\ncunts\nwhore\nwhores\nslut\nsluts\n";
                Files.write(defaultWords, bannedWordsFile, Charset.forName("UTF-8"));
            }

            return Files.readLines(bannedWordsFile, Charset.forName("UTF-8"));
        } catch (Exception e) {
            GriefPrevention.addLogEntry("Failed to read from the banned words data file: " + e.toString());
            e.printStackTrace();
            return new ArrayList<String>();
        }
    }

    // updates soft mute map and data file
    public boolean toggleSoftMute(UUID playerID) {
        boolean newValue = !this.isSoftMuted(playerID);

        this.softMuteMap.put(playerID, newValue);
        this.saveSoftMutes();

        return newValue;
    }

    public boolean isSoftMuted(UUID playerID) {
        Boolean mapEntry = this.softMuteMap.get(playerID);
        if (mapEntry == null || mapEntry == Boolean.FALSE) {
            return false;
        }

        return true;
    }

    private void saveSoftMutes() {
        BufferedWriter outStream = null;

        try {
            // open the file and write the new value
            File softMuteFile = softMuteFilePath.toFile();
            softMuteFile.createNewFile();
            outStream = new BufferedWriter(new FileWriter(softMuteFile));

            for (Map.Entry<UUID, Boolean> entry : softMuteMap.entrySet()) {
                if (entry.getValue().booleanValue()) {
                    outStream.write(entry.getKey().toString());
                    outStream.newLine();
                }
            }

        }

        // if any problem, log it
        catch (Exception e) {
            GriefPrevention.addLogEntry("Unexpected exception saving soft mute data: " + e.getMessage());
            e.printStackTrace();
        }

        // close the file
        try {
            if (outStream != null) {
                outStream.close();
            }
        } catch (IOException exception) {
        }
    }

    // removes cached player data from memory
    public void clearCachedPlayerData(WorldProperties worldProperties, UUID playerUniqueId) {
        this.getClaimWorldManager(worldProperties).removePlayer(playerUniqueId);
    }

    // gets the number of bonus blocks a player has from his permissions
    // this will return 0 when he's offline, and the correct number when online.
    public int getGroupBonusBlocks(UUID playerID) {
        int bonusBlocks = 0;
        Set<String> keys = permissionToBonusBlocksMap.keySet();
        Iterator<String> iterator = keys.iterator();
        while (iterator.hasNext()) {
            String groupName = iterator.next();
            Optional<Player> player = Sponge.getGame().getServer().getPlayer(playerID);
            if (player.isPresent() && player.get().hasPermission(groupName)) {
                bonusBlocks += this.permissionToBonusBlocksMap.get(groupName);
            }
        }

        return bonusBlocks;
    }

    // grants a group (players with a specific permission) bonus claim blocks as
    // long as they're still members of the group
    public int adjustGroupBonusBlocks(String groupName, int amount) {
        Integer currentValue = this.permissionToBonusBlocksMap.get(groupName);
        if (currentValue == null) {
            currentValue = 0;
        }

        currentValue += amount;
        this.permissionToBonusBlocksMap.put(groupName, currentValue);

        // write changes to storage to ensure they don't get lost
        this.saveGroupBonusBlocks(groupName, currentValue);

        return currentValue;
    }

    abstract void saveGroupBonusBlocks(String groupName, int amount);

    // adds a claim to the datastore, making it an effective claim
    void addClaim(Claim newClaim, boolean writeToStorage) {
        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(newClaim.world.getProperties());
        claimWorldManager.addWorldClaim(newClaim);

        newClaim.inDataStore = true;

        // make sure the claim is saved to disk
        if (writeToStorage) {
            this.writeClaimToStorage(newClaim);
        }
    }

    abstract void writeClaimToStorage(Claim claim);

    // increments the claim ID and updates secondary storage to be sure it's
    // saved
    abstract void incrementNextClaimID();

    // deletes a claim or subdivision
    public void deleteClaim(Claim claim) {
        this.deleteClaim(claim, true);
    }

    public void deleteClaim(Claim claim, boolean fireEvent) {
        // delete any children
        for (int j = 0; j < claim.children.size(); j++) {
            this.deleteClaim(claim.children.get(j--), true);
        }

        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(claim.world.getProperties());
        // subdivisions must also be removed from the parent claim child list
        if (claim.parent != null) {
            Claim parentClaim = claim.parent;
            parentClaim.children.remove(claim);
            parentClaim.getClaimStorage().getConfig().getSubdivisions().remove(claim.id);
            // force a save for deletes
            parentClaim.getClaimData().setRequiresSave(true);
            parentClaim.getClaimStorage().save();
        } else {
            claimWorldManager.removePlayerClaim(claim);
            this.deleteClaimFromSecondaryStorage(claim);
        }

        ArrayList<String> chunkStrings = claim.getChunkStrings();
        for (String chunkString : chunkStrings) {
            List<Claim> claimsInChunk = claimWorldManager.getChunksToClaimsMap().get(chunkString);
            for (int j = 0; j < claimsInChunk.size(); j++) {
                if (claimsInChunk.get(j).id.equals(claim.id)) {
                    claimsInChunk.remove(j);
                    break;
                }
            }
        }

        // mark as deleted so any references elsewhere can be ignored
        claim.inDataStore = false;

        if (fireEvent) {
            ClaimDeletedEvent ev = new ClaimDeletedEvent(claim);
            Sponge.getGame().getEventManager().post(ev);
        }
    }

    abstract void deleteClaimFromSecondaryStorage(Claim claim);

    // gets the claim at a specific location
    // ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim
    // cachedClaim can be NULL, but will help performance if you have a
    // reasonable guess about which claim the location is in
    public Claim getClaimAt(Location<World> location, boolean ignoreHeight, Claim cachedClaim) {
        // check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done
        if (cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) {
            return cachedClaim;
        }

        // find a top level claim
        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(location.getExtent().getProperties());
        if (claimWorldManager == null) {
            return null;
        }

        String chunkID = this.getChunkString(location);
        List<Claim> claimsInChunk = claimWorldManager.getChunksToClaimsMap().get(chunkID);
        if (claimsInChunk == null) {
            return null;
        }

        for (Claim claim : claimsInChunk) {
            if (claim.inDataStore && claim.contains(location, ignoreHeight, false)) {
                // when we find a top level claim, if the location is in one of its subdivisions,
                // return the SUBDIVISION, not the top level claim
                for (int j = 0; j < claim.children.size(); j++) {
                    Claim subdivision = claim.children.get(j);
                    if (subdivision.inDataStore && subdivision.contains(location, ignoreHeight, false)) {
                        return subdivision;
                    }
                }

                return claim;
            }
        }

        // if no claim found, return null
        return null;
    }

    // finds a claim by ID
    public Claim getClaim(World world, UUID id) {
        return this.getClaimWorldManager(world.getProperties()).getClaimByUUID(id);
    }

    // gets an almost-unique, persistent identifier string for a chunk
    String getChunkString(Location<World> location) {
        return String.valueOf(location.getBlockX() >> 4) + (location.getBlockZ() >> 4);
    }

    // creates a claim.
    // if the new claim would overlap an existing claim, returns a failure along
    // with a reference to the existing claim
    // if the new claim would overlap a WorldGuard region where the player
    // doesn't have permission to build, returns a failure with NULL for claim
    // otherwise, returns a success along with a reference to the new claim
    // use ownerName == "" for administrative claims for top level claims, pass parent == NULL
    // DOES adjust claim blocks available on success (players can go into negative quantity available)
    // DOES check for world guard regions where the player doesn't have permission
    // does NOT check a player has permission to create a claim, or enough claim blocks.
    // does NOT check minimum claim size constraints
    // does NOT visualize the new claim for any players
    public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, UUID claimId, Claim parent, Player player) {
        CreateClaimResult result = new CreateClaimResult();

        int smallx, bigx, smally, bigy, smallz, bigz;

        // determine small versus big inputs
        if (x1 < x2) {
            smallx = x1;
            bigx = x2;
        } else {
            smallx = x2;
            bigx = x1;
        }

        if (y1 < y2) {
            smally = y1;
            bigy = y2;
        } else {
            smally = y2;
            bigy = y1;
        }

        if (z1 < z2) {
            smallz = z1;
            bigz = z2;
        } else {
            smallz = z2;
            bigz = z1;
        }

        // creative mode claims always go to bedrock
        if (GriefPrevention.instance.claimModeIsActive(world.getProperties(), ClaimsMode.Creative)) {
            smally = 2;
        }

        // create a new claim instance (but don't save it, yet)
        Claim newClaim = new Claim(
                new Location<World>(world, smallx, smally, smallz),
                new Location<World>(world, bigx, bigy, bigz),
                claimId, player);

        newClaim.parent = parent;
        // ensure this new claim won't overlap any existing claims
        ArrayList<Claim> claimsToCheck = null;
        if (newClaim.parent != null) {
            newClaim.type = Claim.Type.SUBDIVISION;
            newClaim.setClaimStorage(newClaim.parent.getClaimStorage());
            SubDivisionDataConfig subData = new SubDivisionDataConfig(newClaim);
            newClaim.setClaimData(subData);
            newClaim.getClaimStorage().getConfig().getSubdivisions().put(claimId, subData);
            newClaim.parent.children.add(newClaim);
            newClaim.parent.getClaimStorage().getConfig().setRequiresSave(true);
            newClaim.parent.getClaimStorage().save();
            newClaim.inDataStore = true;
        } else {
            claimsToCheck = (ArrayList<Claim>) this.getClaimWorldManager(world.getProperties()).getWorldClaims();
            newClaim.ownerID = player.getUniqueId();
        }

        if (claimsToCheck != null) {
            for (int i = 0; i < claimsToCheck.size(); i++) {
                Claim otherClaim = claimsToCheck.get(i);

                // if we find an existing claim which will be overlapped
                if (otherClaim.id != newClaim.id && otherClaim.inDataStore && otherClaim.overlaps(newClaim)) {
                    // result = fail, return conflicting claim
                    result.succeeded = false;
                    result.claim = otherClaim;
                    return result;
                }
            }
            // otherwise add this new claim to the data store to make it effective
            this.addClaim(newClaim, true);
        }

        newClaim.context = new Context("claim", newClaim.id.toString());
        // Assign owner full flag permissions in claim context
        player.getSubjectData().setPermission(ImmutableSet.of(newClaim.getContext()), GPPermissions.CLAIM_MANAGE_FLAGS, Tristate.TRUE);
        // then return success along with reference to new claim
        result.succeeded = true;
        result.claim = newClaim;
        return result;
    }

    public void asyncSaveGlobalPlayerData(UUID playerID, PlayerData playerData) {
        // save everything except the ignore list
        this.overrideSavePlayerData(playerID, playerData);

        // save the ignore list
        if (playerData.ignoreListChanged) {
            StringBuilder fileContent = new StringBuilder();
            try {
                for (UUID uuidKey : playerData.ignoredPlayers.keySet()) {
                    Boolean value = playerData.ignoredPlayers.get(uuidKey);
                    if (value == null) {
                        continue;
                    }

                    // admin-enforced ignores begin with an asterisk
                    if (value) {
                        fileContent.append("*");
                    }

                    fileContent.append(uuidKey);<commit_msg>Don't set permissions when claims are created.<commit_after>/*
 * This file is part of GriefPrevention, licensed under the MIT License (MIT).
 *
 * Copyright (c) <NAME>
 * 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.ryanhamshire.griefprevention;

import com.flowpowered.math.vector.Vector3d;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import me.ryanhamshire.griefprevention.claim.Claim;
import me.ryanhamshire.griefprevention.claim.ClaimsMode;
import me.ryanhamshire.griefprevention.claim.CreateClaimResult;
import me.ryanhamshire.griefprevention.configuration.ClaimTemplateStorage;
import me.ryanhamshire.griefprevention.configuration.GriefPreventionConfig;
import me.ryanhamshire.griefprevention.configuration.SubDivisionDataConfig;
import me.ryanhamshire.griefprevention.configuration.types.DimensionConfig;
import me.ryanhamshire.griefprevention.configuration.types.GlobalConfig;
import me.ryanhamshire.griefprevention.configuration.types.WorldConfig;
import me.ryanhamshire.griefprevention.event.ClaimDeletedEvent;
import me.ryanhamshire.griefprevention.task.SecureClaimTask;
import me.ryanhamshire.griefprevention.task.SiegeCheckupTask;
import net.minecraft.item.ItemStack;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
import org.apache.commons.lang3.StringUtils;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.entity.living.player.User;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.service.context.Context;
import org.spongepowered.api.service.user.UserStorageService;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.action.TextActions;
import org.spongepowered.api.text.format.TextColor;
import org.spongepowered.api.text.format.TextColors;
import org.spongepowered.api.util.Tristate;
import org.spongepowered.api.world.Chunk;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.storage.WorldProperties;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

//singleton class which manages all GriefPrevention data (except for config options)
public abstract class DataStore {

    // World UUID -> PlayerDataWorldManager
    protected final Map<UUID, ClaimWorldManager> claimWorldManagers = Maps.newHashMap();
    // Global PlayerData manager
    protected ClaimWorldManager globalClaimWorldManager;

    // in-memory cache for group (permission-based) data
    protected ConcurrentHashMap<String, Integer> permissionToBonusBlocksMap = new ConcurrentHashMap<>();

    // in-memory cache for claim data
    public static Map<UUID, GriefPreventionConfig<DimensionConfig>> dimensionConfigMap = Maps.newHashMap();
    public static Map<UUID, GriefPreventionConfig<WorldConfig>> worldConfigMap = Maps.newHashMap();
    public static TreeMap<String, ClaimTemplateStorage> globalTemplates = new TreeMap<>();
    public static GriefPreventionConfig<GlobalConfig> globalConfig;

    // in-memory cache for messages
    protected EnumMap<Messages, CustomizableMessage> messages = new EnumMap<>(Messages.class);

    // pattern for unique user identifiers (UUIDs)
    protected final static Pattern uuidpattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");

    // path information, for where stuff stored on disk is well... stored
    protected final static Path dataLayerFolderPath = Paths.get("config").resolve("GriefPrevention");
    final static Path globalDataPath = Paths.get("GriefPreventionData").resolve("GlobalPlayerData");
    final static Path messagesFilePath = dataLayerFolderPath.resolve("messages.conf");
    final static Path softMuteFilePath = dataLayerFolderPath.resolve("softMute.txt");
    final static Path bannedWordsFilePath = dataLayerFolderPath.resolve("bannedWords.txt");
    public Path globalPlayerDataPath;

    // the latest version of the data schema implemented here
    protected static final int latestSchemaVersion = 2;

    // reading and writing the schema version to the data store
    abstract int getSchemaVersionFromStorage();

    abstract void updateSchemaVersionInStorage(int versionToSet);

    // current version of the schema of data in secondary storage
    private int currentSchemaVersion = -1; // -1 means not determined yet

    // video links
    public static final String SURVIVAL_VIDEO_URL_RAW = "http://bit.ly/mcgpuser";
    static final String CREATIVE_VIDEO_URL_RAW = "http://bit.ly/mcgpcrea";
    static final String SUBDIVISION_VIDEO_URL_RAW = "http://bit.ly/mcgpsub";

    public static boolean generateMessages = true;

    // list of UUIDs which are soft-muted
    ConcurrentHashMap<UUID, Boolean> softMuteMap = new ConcurrentHashMap<UUID, Boolean>();

    protected int getSchemaVersion() {
        if (this.currentSchemaVersion >= 0) {
            return this.currentSchemaVersion;
        } else {
            this.currentSchemaVersion = this.getSchemaVersionFromStorage();
            return this.currentSchemaVersion;
        }
    }

    protected void setSchemaVersion(int versionToSet) {
        this.currentSchemaVersion = versionToSet;
        this.updateSchemaVersionInStorage(versionToSet);
    }

    // initialization!
    void initialize() throws Exception {
        // ensure global player data folder exists
        if (GriefPrevention.getGlobalConfig().getConfig().playerdata.useGlobalPlayerDataStorage) {
            this.globalClaimWorldManager = new ClaimWorldManager();
        }

        this.globalPlayerDataPath = Sponge.getGame().getSavesDirectory().resolve(Sponge.getServer().getDefaultWorldName()).resolve(globalDataPath);
        File globalPlayerDataFolder = this.globalPlayerDataPath.toFile();
        if (!globalPlayerDataFolder.exists()) {
            globalPlayerDataFolder.mkdirs();
        }

        // load up all the messages from messages.hocon
        this.loadMessages();
        GriefPrevention.addLogEntry("Customizable messages loaded.");

        // load list of soft mutes
        this.loadSoftMutes();

        // make a note of the data store schema version
        this.setSchemaVersion(latestSchemaVersion);
    }

    private void loadSoftMutes() {
        File softMuteFile = softMuteFilePath.toFile();
        if (softMuteFile.exists()) {
            BufferedReader inStream = null;
            try {
                // open the file
                inStream = new BufferedReader(new FileReader(softMuteFile.getAbsolutePath()));

                // while there are lines left
                String nextID = inStream.readLine();
                while (nextID != null) {
                    // parse line into a UUID
                    UUID playerID;
                    try {
                        playerID = UUID.fromString(nextID);
                    } catch (Exception e) {
                        playerID = null;
                        GriefPrevention.addLogEntry("Failed to parse soft mute entry as a UUID: " + nextID);
                    }

                    // push it into the map
                    if (playerID != null) {
                        this.softMuteMap.put(playerID, true);
                    }

                    // move to the next
                    nextID = inStream.readLine();
                }
            } catch (Exception e) {
                GriefPrevention.addLogEntry("Failed to read from the soft mute data file: " + e.toString());
                e.printStackTrace();
            }

            try {
                if (inStream != null) {
                    inStream.close();
                }
            } catch (IOException exception) {
            }
        }
    }

    public List<String> loadBannedWords() {
        try {
            File bannedWordsFile = bannedWordsFilePath.toFile();
            if (!bannedWordsFile.exists()) {
                Files.touch(bannedWordsFile);
                String defaultWords =
                        "nigger\nniggers\nniger\nnigga\nnigers\nniggas\n" +
                                "fag\nfags\nfaggot\nfaggots\nfeggit\nfeggits\nfaggit\nfaggits\n" +
                                "cunt\ncunts\nwhore\nwhores\nslut\nsluts\n";
                Files.write(defaultWords, bannedWordsFile, Charset.forName("UTF-8"));
            }

            return Files.readLines(bannedWordsFile, Charset.forName("UTF-8"));
        } catch (Exception e) {
            GriefPrevention.addLogEntry("Failed to read from the banned words data file: " + e.toString());
            e.printStackTrace();
            return new ArrayList<String>();
        }
    }

    // updates soft mute map and data file
    public boolean toggleSoftMute(UUID playerID) {
        boolean newValue = !this.isSoftMuted(playerID);

        this.softMuteMap.put(playerID, newValue);
        this.saveSoftMutes();

        return newValue;
    }

    public boolean isSoftMuted(UUID playerID) {
        Boolean mapEntry = this.softMuteMap.get(playerID);
        if (mapEntry == null || mapEntry == Boolean.FALSE) {
            return false;
        }

        return true;
    }

    private void saveSoftMutes() {
        BufferedWriter outStream = null;

        try {
            // open the file and write the new value
            File softMuteFile = softMuteFilePath.toFile();
            softMuteFile.createNewFile();
            outStream = new BufferedWriter(new FileWriter(softMuteFile));

            for (Map.Entry<UUID, Boolean> entry : softMuteMap.entrySet()) {
                if (entry.getValue().booleanValue()) {
                    outStream.write(entry.getKey().toString());
                    outStream.newLine();
                }
            }

        }

        // if any problem, log it
        catch (Exception e) {
            GriefPrevention.addLogEntry("Unexpected exception saving soft mute data: " + e.getMessage());
            e.printStackTrace();
        }

        // close the file
        try {
            if (outStream != null) {
                outStream.close();
            }
        } catch (IOException exception) {
        }
    }

    // removes cached player data from memory
    public void clearCachedPlayerData(WorldProperties worldProperties, UUID playerUniqueId) {
        this.getClaimWorldManager(worldProperties).removePlayer(playerUniqueId);
    }

    // gets the number of bonus blocks a player has from his permissions
    // this will return 0 when he's offline, and the correct number when online.
    public int getGroupBonusBlocks(UUID playerID) {
        int bonusBlocks = 0;
        Set<String> keys = permissionToBonusBlocksMap.keySet();
        Iterator<String> iterator = keys.iterator();
        while (iterator.hasNext()) {
            String groupName = iterator.next();
            Optional<Player> player = Sponge.getGame().getServer().getPlayer(playerID);
            if (player.isPresent() && player.get().hasPermission(groupName)) {
                bonusBlocks += this.permissionToBonusBlocksMap.get(groupName);
            }
        }

        return bonusBlocks;
    }

    // grants a group (players with a specific permission) bonus claim blocks as
    // long as they're still members of the group
    public int adjustGroupBonusBlocks(String groupName, int amount) {
        Integer currentValue = this.permissionToBonusBlocksMap.get(groupName);
        if (currentValue == null) {
            currentValue = 0;
        }

        currentValue += amount;
        this.permissionToBonusBlocksMap.put(groupName, currentValue);

        // write changes to storage to ensure they don't get lost
        this.saveGroupBonusBlocks(groupName, currentValue);

        return currentValue;
    }

    abstract void saveGroupBonusBlocks(String groupName, int amount);

    // adds a claim to the datastore, making it an effective claim
    void addClaim(Claim newClaim, boolean writeToStorage) {
        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(newClaim.world.getProperties());
        claimWorldManager.addWorldClaim(newClaim);

        newClaim.inDataStore = true;

        // make sure the claim is saved to disk
        if (writeToStorage) {
            this.writeClaimToStorage(newClaim);
        }
    }

    abstract void writeClaimToStorage(Claim claim);

    // increments the claim ID and updates secondary storage to be sure it's
    // saved
    abstract void incrementNextClaimID();

    // deletes a claim or subdivision
    public void deleteClaim(Claim claim) {
        this.deleteClaim(claim, true);
    }

    public void deleteClaim(Claim claim, boolean fireEvent) {
        // delete any children
        for (int j = 0; j < claim.children.size(); j++) {
            this.deleteClaim(claim.children.get(j--), true);
        }

        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(claim.world.getProperties());
        // subdivisions must also be removed from the parent claim child list
        if (claim.parent != null) {
            Claim parentClaim = claim.parent;
            parentClaim.children.remove(claim);
            parentClaim.getClaimStorage().getConfig().getSubdivisions().remove(claim.id);
            // force a save for deletes
            parentClaim.getClaimData().setRequiresSave(true);
            parentClaim.getClaimStorage().save();
        } else {
            claimWorldManager.removePlayerClaim(claim);
            this.deleteClaimFromSecondaryStorage(claim);
        }

        ArrayList<String> chunkStrings = claim.getChunkStrings();
        for (String chunkString : chunkStrings) {
            List<Claim> claimsInChunk = claimWorldManager.getChunksToClaimsMap().get(chunkString);
            for (int j = 0; j < claimsInChunk.size(); j++) {
                if (claimsInChunk.get(j).id.equals(claim.id)) {
                    claimsInChunk.remove(j);
                    break;
                }
            }
        }

        // mark as deleted so any references elsewhere can be ignored
        claim.inDataStore = false;

        if (fireEvent) {
            ClaimDeletedEvent ev = new ClaimDeletedEvent(claim);
            Sponge.getGame().getEventManager().post(ev);
        }
    }

    abstract void deleteClaimFromSecondaryStorage(Claim claim);

    // gets the claim at a specific location
    // ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim
    // cachedClaim can be NULL, but will help performance if you have a
    // reasonable guess about which claim the location is in
    public Claim getClaimAt(Location<World> location, boolean ignoreHeight, Claim cachedClaim) {
        // check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done
        if (cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) {
            return cachedClaim;
        }

        // find a top level claim
        ClaimWorldManager claimWorldManager = this.getClaimWorldManager(location.getExtent().getProperties());
        if (claimWorldManager == null) {
            return null;
        }

        String chunkID = this.getChunkString(location);
        List<Claim> claimsInChunk = claimWorldManager.getChunksToClaimsMap().get(chunkID);
        if (claimsInChunk == null) {
            return null;
        }

        for (Claim claim : claimsInChunk) {
            if (claim.inDataStore && claim.contains(location, ignoreHeight, false)) {
                // when we find a top level claim, if the location is in one of its subdivisions,
                // return the SUBDIVISION, not the top level claim
                for (int j = 0; j < claim.children.size(); j++) {
                    Claim subdivision = claim.children.get(j);
                    if (subdivision.inDataStore && subdivision.contains(location, ignoreHeight, false)) {
                        return subdivision;
                    }
                }

                return claim;
            }
        }

        // if no claim found, return null
        return null;
    }

    // finds a claim by ID
    public Claim getClaim(World world, UUID id) {
        return this.getClaimWorldManager(world.getProperties()).getClaimByUUID(id);
    }

    // gets an almost-unique, persistent identifier string for a chunk
    String getChunkString(Location<World> location) {
        return String.valueOf(location.getBlockX() >> 4) + (location.getBlockZ() >> 4);
    }

    // creates a claim.
    // if the new claim would overlap an existing claim, returns a failure along
    // with a reference to the existing claim
    // if the new claim would overlap a WorldGuard region where the player
    // doesn't have permission to build, returns a failure with NULL for claim
    // otherwise, returns a success along with a reference to the new claim
    // use ownerName == "" for administrative claims for top level claims, pass parent == NULL
    // DOES adjust claim blocks available on success (players can go into negative quantity available)
    // DOES check for world guard regions where the player doesn't have permission
    // does NOT check a player has permission to create a claim, or enough claim blocks.
    // does NOT check minimum claim size constraints
    // does NOT visualize the new claim for any players
    public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, UUID claimId, Claim parent, Player player) {
        CreateClaimResult result = new CreateClaimResult();

        int smallx, bigx, smally, bigy, smallz, bigz;

        // determine small versus big inputs
        if (x1 < x2) {
            smallx = x1;
            bigx = x2;
        } else {
            smallx = x2;
            bigx = x1;
        }

        if (y1 < y2) {
            smally = y1;
            bigy = y2;
        } else {
            smally = y2;
            bigy = y1;
        }

        if (z1 < z2) {
            smallz = z1;
            bigz = z2;
        } else {
            smallz = z2;
            bigz = z1;
        }

        // creative mode claims always go to bedrock
        if (GriefPrevention.instance.claimModeIsActive(world.getProperties(), ClaimsMode.Creative)) {
            smally = 2;
        }

        // create a new claim instance (but don't save it, yet)
        Claim newClaim = new Claim(
                new Location<World>(world, smallx, smally, smallz),
                new Location<World>(world, bigx, bigy, bigz),
                claimId, player);

        newClaim.parent = parent;
        // ensure this new claim won't overlap any existing claims
        ArrayList<Claim> claimsToCheck = null;
        if (newClaim.parent != null) {
            newClaim.type = Claim.Type.SUBDIVISION;
            newClaim.setClaimStorage(newClaim.parent.getClaimStorage());
            SubDivisionDataConfig subData = new SubDivisionDataConfig(newClaim);
            newClaim.setClaimData(subData);
            newClaim.getClaimStorage().getConfig().getSubdivisions().put(claimId, subData);
            newClaim.parent.children.add(newClaim);
            newClaim.parent.getClaimStorage().getConfig().setRequiresSave(true);
            newClaim.parent.getClaimStorage().save();
            newClaim.inDataStore = true;
        } else {
            claimsToCheck = (ArrayList<Claim>) this.getClaimWorldManager(world.getProperties()).getWorldClaims();
            newClaim.ownerID = player.getUniqueId();
        }

        if (claimsToCheck != null) {
            for (int i = 0; i < claimsToCheck.size(); i++) {
                Claim otherClaim = claimsToCheck.get(i);

                // if we find an existing claim which will be overlapped
                if (otherClaim.id != newClaim.id && otherClaim.inDataStore && otherClaim.overlaps(newClaim)) {
                    // result = fail, return conflicting claim
                    result.succeeded = false;
                    result.claim = otherClaim;
                    return result;
                }
            }
            // otherwise add this new claim to the data store to make it effective
            this.addClaim(newClaim, true);
        }

        newClaim.context = new Context("claim", newClaim.id.toString());
        // return success along with reference to new claim
        result.succeeded = true;
        result.claim = newClaim;
        return result;
    }

    public void asyncSaveGlobalPlayerData(UUID playerID, PlayerData playerData) {
        // save everything except the ignore list
        this.overrideSavePlayerData(playerID, playerData);

        // save the ignore list
        if (playerData.ignoreListChanged) {
            StringBuilder fileContent = new StringBuilder();
            try {
                for (UUID uuidKey : playerData.ignoredPlayers.keySet()) {
                    Boolean value = playerData.ignoredPlayers.get(uuidKey);
                    if (value == null) {
                        continue;
                    }

                    // admin-enforced ignores begin with an asterisk
                    if (value) {
                        fileContent.append("*");
                    }

                    fileContent.append(uuidKey);
{'fraction_non_alphanumeric': 0.08869184361380081, 'fraction_numerical': 0.002490499602808253, 'mean_word_length': 2.4591904938730043, 'pattern_counts': {'":': 0, '<': 77, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6031825', 'n_tokens_mistral': 12440, 'n_tokens_neox': 11916, 'n_words': 3399}