/*
 * Copyright 2011 Tyler Blair. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and contributors and should not be interpreted as representing official policies,
 * either expressed or implied, of anybody else.
 */

package com.griefcraft.model;

import com.griefcraft.cache.BlockCache;
import com.griefcraft.cache.ProtectionCache;
import com.griefcraft.lwc.LWC;
import com.griefcraft.scripting.event.LWCProtectionRemovePostEvent;
import com.griefcraft.util.Colors;
import com.griefcraft.util.ProtectionFinder;
import com.griefcraft.util.StringUtil;
import com.griefcraft.util.TimeUtil;
import com.griefcraft.util.UUIDRegistry;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public class Protection {

    /**
     * The protection type
     * <p>
     * <p>
     * Ordering <b>must NOT change</b> as ordinal values are used
     * </p>
     */
    public enum Type {

        /**
         * The protection is usable by anyone; the most common use would include
         * community chests where anyone can use the chest but no one should be
         * able to protect as their own.
         */
        PUBLIC,

        /**
         * The owner (and anyone else) must enter a set password entered onto
         * the chest in order to be able to access it. Entering the correct
         * password allows them to use the chest until they log out or the
         * protection is removed.
         */
        PASSWORD,

        /**
         * The protection is only usable by the player who created it. Further
         * access can be given to players, groups, and even more specific
         * entities such as Towns in the "Towny" plugin, or access lists via the
         * "Lists" plugin
         */
        PRIVATE,

        /**
         * Reserved / unused, to keep ordinal order
         */
        RESERVED1,

        /**
         * Reserved / unused, to keep ordinal order
         */
        RESERVED2,

        /**
         * Allows players to deposit items into
         */
        DONATION,

        /**
         * Allows players to look into but not take
         */
        DISPLAY,

        /**
         * Allows players to take items from but not deposit
         */
        SUPPLY;

        /**
         * Match a protection type using its string form
         *
         * @param text
         * @return
         */
        public static Type matchType(String text) {
            for (Type type : values()) {
                if (type.toString().equalsIgnoreCase(text)) {
                    return type;
                }
            }

            throw new IllegalArgumentException(
                    "No Protection Type found for given type: " + text);
        }

    }

    /**
     * All of the history items associated with this protection
     */
    private final Set<History> historyCache = new HashSet<>();

    /**
     * List of the permissions rights for the protection
     */
    private final Set<Permission> permissions = new HashSet<>();

    /**
     * List of flags enabled on the protection
     */
    private final Map<Flag.Type, Flag> flags = new HashMap<>();

    /**
     * The block id
     */
    private int blockId;

    /**
     * The password for the chest
     */
    private String password;

    /**
     * JSON data for the protection
     */
    private final JSONObject data = new JSONObject();

    /**
     * Unique id (in sql)
     */
    private int id;

    /**
     * The owner of the chest
     */
    private String owner;

    /**
     * The protection type
     */
    private Type type;

    /**
     * The world the protection is in
     */
    private String world;

    /**
     * The x coordinate
     */
    private int x;

    /**
     * The y coordinate
     */
    private int y;

    /**
     * The z coordinate
     */
    private int z;

    /**
     * The timestamp of when the protection was last accessed
     */
    private long lastAccessed;

    /**
     * The time the protection was created
     */
    private String creation;

    /**
     * Immutable flag for the protection. When removed, this bool is switched to
     * true and any setters will no longer work. However, everything is still
     * intact and in memory at this point (for now.)
     */
    private boolean removed = false;

    /**
     * If the protection is pending removal. Only used internally.
     */
    private boolean removing = false;

    /**
     * True when the protection has been modified and should be saved
     */
    private boolean modified = false;

    /**
     * The protection finder used to find this protection
     */
    private ProtectionFinder finder;

    /**
     * The block the protection is at. Saves world calls and allows better
     * concurrency
     */
    private Block cachedBlock;

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof Protection)) {
            return false;
        }

        Protection other = (Protection) object;

        return id == other.id && x == other.x && y == other.y && z == other.z
                && (owner != null && owner.equals(other.owner))
                && (world != null && world.equals(other.world));
    }

    @Override
    public int hashCode() {
        int hash = 17;

        // the identifier is normally unique, but in SQLite ids may be quickly
        // reused so we use other data
        hash *= 37 + id;

        // coordinates
        hash *= 37 + x;
        hash *= 37 + y;
        hash *= 37 + z;

        // and for good measure, to *guarantee* no collisions
        if (creation != null) {
            hash *= 37 + creation.hashCode();
        }

        return hash;
    }

    /**
     * Convert the protection to use UUIDs
     *
     * @return true if the protection required conversion and conversions were
     * done
     */
    public boolean convertPlayerNamesToUUIDs() {
        if (!needsUUIDConversion()) {
            return false;
        }

        boolean res = false;

        if (!UUIDRegistry.isValidUUID(owner)) {
            UUID uuid = UUIDRegistry.getUUID(owner);

            if (uuid != null) {
                setOwner(uuid.toString());
                res = true;
            }
        }

        for (Permission permission : permissions) {
            if (permission.getType() == Permission.Type.PLAYER
                    && !UUIDRegistry.isValidUUID(permission.getName())) {
                UUID uuid = UUIDRegistry.getUUID(permission.getName());

                if (uuid != null) {
                    permission.setName(uuid.toString());
                    modified = true;
                    res = true;
                }
            }
        }

        return res;
    }

    /**
     * Check if this protection requires conversion from plain player names to
     * UUIDs
     *
     * @return true if the protection requires conversion
     */
    public boolean needsUUIDConversion() {
        if (!UUIDRegistry.isValidUUID(owner)) {
            return true;
        }

        for (Permission permission : permissions) {
            if (permission.getType() == Permission.Type.PLAYER
                    && !UUIDRegistry.isValidUUID(permission.getName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get a formatted version of the owner's name. If the owner is a UUID and
     * the UUID is unknown, then "Unknown (uuid)" will be returned.
     *
     * @return
     */
    public String getFormattedOwnerPlayerName() {
        return UUIDRegistry.formatPlayerName(owner);
    }

    /**
     * Encode the AccessRights to JSON
     */
    @SuppressWarnings("unchecked")
    public void encodeRights() {
        // create the root
        JSONArray root = new JSONArray();

        // add all of the permissions to the root
        for (Permission permission : permissions) {
            if (permission != null) {
                root.add(permission.encodeToJSON());
            }
        }

        data.put("rights", root);
    }

    /**
     * Encode the protection flags to JSON
     */
    @SuppressWarnings("unchecked")
    public void encodeFlags() {
        JSONArray root = new JSONArray();

        for (Flag flag : flags.values()) {
            if (flag != null) {
                root.add(flag.getData());
            }
        }

        data.put("flags", root);
    }

    /**
     * Ensure a history object is located in our cache
     *
     * @param history
     */
    public void checkHistory(History history) {
        if (!historyCache.contains(history)) {
            historyCache.add(history);
        }
    }

    /**
     * Check if a player has owner access to the protection
     *
     * @param player
     * @return
     */
    public boolean isOwner(Player player) {
        LWC lwc = LWC.getInstance();

        if (isRealOwner(player)) {
            return true;
        }

        return lwc.isAdmin(player);
    }

    /**
     * Check if a player is the real owner to the protection
     *
     * @param player
     * @return
     */
    public boolean isRealOwner(Player player) {
        if (player == null) {
            return false;
        }

        if (UUIDRegistry.isValidUUID(owner)) {
            return UUID.fromString(owner).equals(player.getUniqueId());
        }

        return owner.equalsIgnoreCase(player.getName());

    }

    /**
     * Create a History object that is attached to this protection
     *
     * @return
     */
    public History createHistoryObject() {
        History history = new History();

        history.setProtectionId(id);
        history.setProtection(this);
        history.setStatus(History.Status.INACTIVE);
        history.setX(x);
        history.setY(y);
        history.setZ(z);

        // add it to the cache
        historyCache.add(history);

        return history;
    }

    /**
     * @return the related history for this protection, which is immutable
     */
    public Set<History> getRelatedHistory() {
        // cache the database's history if we don't have any yet
        if (historyCache.size() == 0) {
            historyCache.addAll(LWC.getInstance().getPhysicalDatabase()
                    .loadHistory(this));
        }

        // now we can return an immutable cache
        return Collections.unmodifiableSet(historyCache);
    }

    /**
     * Get the related history for this protection using the given type
     *
     * @param type
     * @return
     */
    public List<History> getRelatedHistory(History.Type type) {
        List<History> matches = new ArrayList<>();
        Set<History> relatedHistory = getRelatedHistory();

        for (History history : relatedHistory) {
            if (history.getType() == type) {
                matches.add(history);
            }
        }

        return matches;
    }

    /**
     * Check if a flag is enabled
     *
     * @param type
     * @return
     */
    public boolean hasFlag(Flag.Type type) {
        return flags.containsKey(type);
    }

    /**
     * Get the enabled flag for the corresponding type
     *
     * @param type
     * @return
     */
    public Flag getFlag(Flag.Type type) {
        return flags.get(type);
    }

    /**
     * Add a flag to the protection
     *
     * @param flag
     * @return
     */
    public boolean addFlag(Flag flag) {
        if (removed || flag == null) {
            return false;
        }

        if (!flags.containsKey(flag.getType())) {
            flags.put(flag.getType(), flag);
            modified = true;
            return true;
        }

        return false;
    }

    /**
     * Remove a flag from the protection
     *
     * @param flag
     */
    public void removeFlag(Flag flag) {
        if (removed) {
            return;
        }

        flags.remove(flag.getType());
        this.modified = true;
    }

    /**
     * Check if the entity + permissions type exists, and if so return the
     * rights (-1 if it does not exist)
     *
     * @param type
     * @param name
     * @return the permissions the player has
     */
    public Permission.Access getAccess(String name, Permission.Type type) {
        for (Permission permission : permissions) {
            if (permission.getType() == type
                    && permission.getName().equalsIgnoreCase(name)) {
                return permission.getAccess();
            }
        }

        return Permission.Access.NONE;
    }

    /**
     * @return the list of permissions
     */
    public List<Permission> getPermissions() {
        return Collections.unmodifiableList(new ArrayList<Permission>(
                permissions));
    }

    /**
     * Remove temporary permissions rights from the protection
     */
    public void removeTemporaryPermissions() {
        Iterator<Permission> iter = permissions.iterator();

        while (iter.hasNext()) {
            Permission permission = iter.next();

            if (permission.isVolatile()) {
                iter.remove();
            }
        }
    }

    /**
     * Add an permission to the protection
     *
     * @param permission
     */
    public void addPermission(Permission permission) {
        if (removed || permission == null) {
            return;
        }

        // remove any other rights with the same identity
        removePermissions(permission.getName(), permission.getType());

        // now we can safely add it
        permissions.add(permission);
        modified = true;
    }

    /**
     * Remove permissions from the protection that match a name AND type
     *
     * @param name
     * @param type
     */
    public void removePermissions(String name, Permission.Type type) {
        if (removed) {
            return;
        }

        Iterator<Permission> iter = permissions.iterator();

        while (iter.hasNext()) {
            Permission permission = iter.next();

            if ((permission.getName().equals(name) || name.equals("*"))
                    && permission.getType() == type) {
                iter.remove();
                modified = true;
            }
        }
    }

    /**
     * Remove all of the permissions
     */
    public void removeAllPermissions() {
        permissions.clear();
        modified = true;
    }

    /**
     * Checks if the protection has the correct block in the world
     *
     * @return
     */
    public boolean isBlockInWorld() {
        int storedBlockId = getBlockId();
        Block block = getBlock();

        BlockCache blockCache = BlockCache.getInstance();
        switch (block.getType()) {
            case FURNACE:
                return storedBlockId == blockCache.getBlockId(Material.FURNACE);
            default:
                return storedBlockId == blockCache.getBlockId(block);
        }
    }

    public JSONObject getData() {
        return data;
    }

    public int getBlockId() {
        return blockId;
    }

    public String getPassword() {
        return password;
    }

    public String getCreation() {
        return creation;
    }

    public int getId() {
        return id;
    }

    public String getOwner() {
        return owner;
    }

    public Type getType() {
        return type;
    }

    public String getWorld() {
        return world;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    public long getLastAccessed() {
        return lastAccessed;
    }

    public void setBlockId(int blockId) {
        if (removed) {
            return;
        }

        this.blockId = blockId;
        this.modified = true;
    }

    public void setPassword(String password) {
        if (removed) {
            return;
        }

        this.password = password;
        this.modified = true;
    }

    public void setCreation(String creation) {
        if (removed) {
            return;
        }

        this.creation = creation;
        this.modified = true;
    }

    public void setId(int id) {
        if (removed) {
            return;
        }

        this.id = id;
        this.modified = true;
    }

    public void setOwner(String owner) {
        if (removed) {
            return;
        }

        this.owner = owner;
        this.modified = true;
    }

    public void setType(Type type) {
        if (removed) {
            return;
        }

        this.type = type;
        this.modified = true;
    }

    public void setWorld(String world) {
        if (removed) {
            return;
        }

        this.world = world;
        this.modified = true;
    }

    public void setX(int x) {
        if (removed) {
            return;
        }

        this.x = x;
        this.modified = true;
    }

    public void setY(int y) {
        if (removed) {
            return;
        }

        this.y = y;
        this.modified = true;
    }

    public void setZ(int z) {
        if (removed) {
            return;
        }

        this.z = z;
        this.modified = true;
    }

    public void setLastAccessed(long lastAccessed) {
        if (removed) {
            return;
        }

        this.lastAccessed = lastAccessed;
        this.modified = true;
    }

    /**
     * Sets the protection finder used to create this protection
     *
     * @param finder
     */
    public void setProtectionFinder(ProtectionFinder finder) {
        this.finder = finder;
    }

    /**
     * Gets the protection finder used the create this protection
     *
     * @return the ProtectionFinder used to create this protection
     */
    public ProtectionFinder getProtectionFinder() {
        return finder;
    }

    /**
     * Remove the protection from the database
     */
    public void remove() {
        if (removed) {
            return;
        }

        LWC lwc = LWC.getInstance();
        removeTemporaryPermissions();

        // we're removing it, so assume there are no changes
        modified = false;
        removing = true;

        // broadcast the removal event
        // we broadcast before actually removing to give them a chance to use
        // any password that would be removed otherwise
        lwc.getModuleLoader().dispatchEvent(
                new LWCProtectionRemovePostEvent(this));

        // mark related transactions as inactive
        for (History history : getRelatedHistory(History.Type.TRANSACTION)) {
            if (history.getStatus() != History.Status.ACTIVE) {
                continue;
            }

            history.setStatus(History.Status.INACTIVE);
        }

        // ensure all history objects for this protection are saved
        checkAndSaveHistory();

        // make the protection immutable
        removed = true;

        // and now finally remove it from the database
        lwc.getDatabaseThread().removeProtection(this);
        lwc.getPhysicalDatabase().removeProtection(id);
        removeCache();
    }

    /**
     * Remove the protection from cache
     */
    public void removeCache() {
        LWC lwc = LWC.getInstance();
        lwc.getProtectionCache().removeProtection(this);
        radiusRemoveCache();
    }

    /**
     * Remove blocks around the protection in a radius of 3, to account for
     * broken known / null blocks
     */
    public void radiusRemoveCache() {
        ProtectionCache cache = LWC.getInstance().getProtectionCache();

        for (int x = -3; x <= 3; x++) {
            for (int y = -3; y <= 3; y++) {
                for (int z = -3; z <= 3; z++) {
                    String cacheKey = world + ":" + (this.x + x) + ":"
                            + (this.y + y) + ":" + (this.z + z);

                    // get the protection for that entry
                    Protection protection = cache.getProtection(cacheKey);

                    // the ifnull compensates for the block being in the null
                    // cache. It will remove it from that.
                    if ((protection != null && id == protection.getId())
                            || protection == null) {
                        cache.remove(cacheKey);
                    }
                }
            }
        }
    }

    /**
     * Queue the protection to be saved
     */
    public void save() {
        if (removed) {
            return;
        }

        LWC.getInstance().getDatabaseThread().addProtection(this);
    }

    /**
     * Force a protection update to the live database
     */
    public void saveNow() {
        if (removed) {
            return;
        }

        // encode JSON objects
        encodeRights();
        encodeFlags();

        // only save the protection if it was modified
        if (modified && !removing) {
            LWC.getInstance().getPhysicalDatabase().saveProtection(this);
        }

        // check the cache for history updates
        checkAndSaveHistory();
    }

    /**
     * Saves any of the history items for the Protection that have been modified
     */
    public void checkAndSaveHistory() {
        if (removed) {
            return;
        }

        for (History history : getRelatedHistory()) {
            // if the history object was modified we need to save it
            if (history.wasModified()) {
                history.saveNow();
            }
        }
    }

    /**
     * @return the key used for the protection cache
     */
    public String getCacheKey() {
        return world + ":" + x + ":" + y + ":" + z;
    }

    /**
     * @return the Bukkit world the protection should be located in
     */
    public World getBukkitWorld() {
        if (world == null || world.isEmpty()) {
            return Bukkit.getServer().getWorlds().get(0);
        }

        return Bukkit.getServer().getWorld(world);
    }

    /**
     * @return the Bukkit Player object of the owner
     */
    public Player getBukkitOwner() {
        UUID uuid = UUIDRegistry.getUUID(owner);

        if (uuid == null) {
            try {
                uuid = UUID.fromString(owner);
            } catch (Exception e) {
                uuid = null;
            }
        }

        if (uuid == null) {
            return Bukkit.getServer().getPlayer(owner);
        }

        return Bukkit.getServer().getPlayer(uuid);
    }

    /**
     * @return the block representing the protection in the world
     */
    public Block getBlock() {
        if (cachedBlock != null) {
            return cachedBlock;
        }

        World world = getBukkitWorld();

        if (world == null) {
            return null;
        }

        cachedBlock = world.getBlockAt(x, y, z);
        return cachedBlock;
    }

    /**
     * @return
     */
    @Override
    public String toString() {
        // format the flags prettily
        String flagStr = "";

        for (Flag flag : flags.values()) {
            flagStr += flag.toString() + ",";
        }

        if (flagStr.endsWith(",")) {
            flagStr = flagStr.substring(0, flagStr.length() - 1);
        }

        // format the last accessed time
        String lastAccessed = TimeUtil
                .timeToString((System.currentTimeMillis() / 1000L)
                        - this.lastAccessed);

        if (!lastAccessed.equals("Not yet known")) {
            lastAccessed += " ago";
        }

        BlockCache blockCache = BlockCache.getInstance();
        Material blockType = blockCache.getBlockType(blockId);
        return String
                .format("%s %s"
                                + Colors.White
                                + " "
                                + Colors.Dark_Green
                                + "Id=%d Owner=%s Location=[%s %d,%d,%d] Created=%s Flags=%s LastAccessed=%s",
                        typeToString(),
                        (blockType != null ? LWC.materialToString(blockType)
                                : "Not yet cached"), id, owner, world, x, y, z,
                        creation, flagStr, lastAccessed);
    }

    /**
     * Sends information for a given protection
     *
     * @param sender Command Sender
     * @see Protection#toString()
     * @since 2.2.3
     */
    public void sendProtectionInfo(CommandSender sender) {
        LWC lwc = LWC.getInstance();

        // format the flags prettily
        StringBuilder flagStr = new StringBuilder();

        for (Flag.Type type : Flag.Type.values()) {
            ChatColor color = ChatColor.RED;
            for (Flag flag : flags.values()) {
                if (type.equals(flag.getType())) {
                    color = ChatColor.GREEN;
                    break;
                }
            }
            flagStr.append(color).append(type.toString().toLowerCase()).append(" ");
        }

        if (flagStr.toString().endsWith(" ")) {
            flagStr = new StringBuilder(flagStr.substring(0, flagStr.length() - 1));
        }

        // format the last accessed time
        String lastAccessed = TimeUtil
                .timeToString((System.currentTimeMillis() / 1000L)
                        - this.lastAccessed);

        lwc.sendLocale(sender, "protection.interact.info.formatted",
                "id", id,
                "type", lwc.getPlugin().getMessageParser().parseMessage(getType().toString().toLowerCase()),
                "block", LWC.materialToString(BlockCache.getInstance().getBlockType(blockId)),
                "name", UUIDRegistry.isValidUUID(getOwner()) ? UUIDRegistry.getName(UUID.fromString(getOwner())) : getOwner(),
                "owner", getOwner(),
                "world", world,
                "x", x,
                "y", y,
                "z", z,
                "creation", creation,
                "accessed", lastAccessed,
                "flags", flagStr.toString())
        ;
    }

    /**
     * @return string representation of the protection type
     */
    public String typeToString() {
        return StringUtil.capitalizeFirstLetter(type.toString());
    }

    /**
     * Updates the protection in the protection cache
     */
    @Deprecated
    public void update() {
        throw new UnsupportedOperationException(
                "Protection.update() is no longer necessary!");
    }

}
