package com.spaceshipproject;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.persistence.PersistentDataType;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Logique metier des spaceships : creation, invocation, decharge, teleportation.
 *
 * Etat en memoire :
 *  - returnLocations : ou redescendre le joueur quand il se trouve sur son ship.
 */
public class SpaceshipManager {

    private final SpaceShipProject plugin;
    private final DatabaseManager db;
    private final ConfigManager cfg;

    /** Position au sol memorisee lors de la teleportation vers un ship (par joueur). */
    private final Map<UUID, Location> returnLocations = new HashMap<>();

    public SpaceshipManager(SpaceShipProject plugin) {
        this.plugin = plugin;
        this.db = plugin.getDatabaseManager();
        this.cfg = plugin.getConfigManager();
    }

    /* ============================ CREATION ============================ */

    /**
     * Cree un nouveau spaceship en base avec :
     *   - le modele "default.json" du dossier models/ s'il existe,
     *   - sinon la plateforme procedurale par defaut.
     * Retourne l'id ou -1 si le quota est atteint.
     */
    public long createNewSpaceship(Player owner) {
        int max = cfg.maxShipsPerPlayer();
        int count = db.countSpaceshipsByOwner(owner.getUniqueId());
        if (max > 0 && count >= max) {
            owner.sendMessage(cfg.msgMaxShips(max));
            return -1;
        }
        db.upsertPlayer(owner);

        String schematic = plugin.getModelManager().loadDefaultModelJson();
        int sx, sy, sz;
        if (schematic != null) {
            int[] size = SchematicHelper.readSize(schematic);
            sx = size[0]; sy = size[1]; sz = size[2];
        } else {
            sx = cfg.defaultSizeX();
            sy = cfg.defaultSizeY();
            sz = cfg.defaultSizeZ();
            schematic = SchematicHelper.buildDefaultSchematic(sx, sy, sz,
                    cfg.defaultPlatformMaterial(),
                    cfg.defaultEdgeMaterial(),
                    cfg.defaultGlassMaterial());
        }

        int[] spawn = SchematicHelper.readSpawn(schematic);
        String name = cfg.defaultShipName().replace("%n%", String.valueOf(count + 1));
        return db.createSpaceship(owner.getUniqueId(), name, sx, sy, sz,
                spawn[0], spawn[1], spawn[2], schematic, "{}");
    }

    /* ============================ STATE ============================ */

    public boolean isLoaded(long shipId) {
        return db.getLoaded(shipId) != null;
    }

    public LoadedSpaceship getLoaded(long shipId) {
        return db.getLoaded(shipId);
    }

    /* ============================ SUMMON ============================ */

    /**
     * Invoque le spaceship au-dessus du joueur. Si deja charge, ne fait rien.
     * Coute {@link ConfigManager#costSummon()} en monnaie Vault (paye par le proprietaire).
     */
    public boolean summon(Player player, Spaceship ship) {
        if (isLoaded(ship.getId())) {
            player.sendMessage(cfg.msgAlreadyLoaded());
            return false;
        }

        // Paiement (sauf admin).
        double cost = cfg.costSummon();
        if (cost > 0 && !player.hasPermission("spaceship.bypass.cost")) {
            EconomyHook eco = plugin.getEconomy();
            if (!eco.isReady()) {
                player.sendMessage(cfg.msgVaultUnavailable());
                return false;
            }
            double balance = eco.getBalance(player.getUniqueId());
            if (balance < cost) {
                player.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(balance)));
                return false;
            }
        }

        World world = player.getWorld();

        // Si le niveau a augmente depuis le dernier chargement, on etend la schematique
        // a la nouvelle taille en marquant les coins avec Verdant Froglight.
        if (ship.getLevel() > ship.getLastLoadedLevel()) {
            int[] target = ShipSizing.sizeForLevel(cfg, ship.getLevel());
            int nx = Math.max(target[0], ship.getSizeX());
            int ny = Math.max(target[1], ship.getSizeY());
            int nz = Math.max(target[2], ship.getSizeZ());
            if (nx != ship.getSizeX() || ny != ship.getSizeY() || nz != ship.getSizeZ()) {
                try {
                    String expanded = SchematicHelper.expandSchematic(
                            ship.getSchematicJson(), nx, ny, nz, Material.VERDANT_FROGLIGHT);
                    ship.setSchematicJson(expanded);
                    ship.setSize(nx, ny, nz);
                } catch (Exception ex) {
                    plugin.getLogger().warning("Erreur expansion ship #" + ship.getId() + " : " + ex.getMessage());
                }
            }
            ship.setLastLoadedLevel(ship.getLevel());
            db.updateSpaceship(ship);
        }

        int sx = ship.getSizeX(), sy = ship.getSizeY(), sz = ship.getSizeZ();
        int ox = player.getLocation().getBlockX() - sx / 2;
        int oz = player.getLocation().getBlockZ() - sz / 2;
        int oy = clampY(world, player.getLocation().getBlockY() + cfg.summonOffsetY(), sy);

        if (cfg.checkCollision() && SchematicHelper.hasCollision(world, ox, oy, oz, sx, sy, sz)) {
            player.sendMessage(cfg.msgCollision());
            return false;
        }

        try {
            SchematicHelper.restoreRegion(ship.getSchematicJson(), world, ox, oy, oz);
        } catch (Exception ex) {
            plugin.getLogger().severe("Erreur restauration ship #" + ship.getId() + " : " + ex.getMessage());
            return false;
        }

        // Debit apres succes de la restauration.
        if (cost > 0 && !player.hasPermission("spaceship.bypass.cost")) {
            plugin.getEconomy().withdraw(player.getUniqueId(), cost);
        }

        db.markLoaded(ship.getId(), world.getName(), ox, oy, oz);
        player.sendMessage(cfg.msgSummoned(ship.getName()));
        return true;
    }

    /* ============================ UNLOAD ============================ */

    /**
     * Capture les modifications et decharge le ship du monde.
     * Sauvegarde la nouvelle schematique en base.
     */
    public boolean unload(Player player, Spaceship ship) {
        LoadedSpaceship loaded = db.getLoaded(ship.getId());
        if (loaded == null) {
            player.sendMessage(cfg.msgNotLoaded());
            return false;
        }
        World world = loaded.getWorld();
        if (world == null) {
            plugin.getLogger().warning("Monde introuvable pour decharger le ship #" + ship.getId());
            db.markUnloaded(ship.getId());
            return false;
        }

        try {
            String updated = SchematicHelper.captureRegion(world,
                    loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                    ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(),
                    ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ());
            ship.setSchematicJson(updated);
            db.updateSpaceship(ship);
        } catch (Exception ex) {
            plugin.getLogger().severe("Erreur capture ship #" + ship.getId() + " : " + ex.getMessage());
            return false;
        }

        SchematicHelper.clearRegion(world,
                loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                ship.getSizeX(), ship.getSizeY(), ship.getSizeZ());

        db.markUnloaded(ship.getId());
        player.sendMessage(cfg.msgUnloaded(ship.getName()));
        return true;
    }

    /* ============================ TELEPORT ============================ */

    /**
     * Teleporte le joueur sur le spawn du ship (le ship doit etre charge).
     * Memorise la position de retour.
     */
    public boolean teleportToShip(Player player, Spaceship ship) {
        LoadedSpaceship loaded = db.getLoaded(ship.getId());
        if (loaded == null) {
            player.sendMessage(cfg.msgNotLoaded());
            return false;
        }
        World world = loaded.getWorld();
        if (world == null) return false;

        returnLocations.put(player.getUniqueId(), player.getLocation().clone());

        Location target = new Location(world,
                loaded.getOriginX() + ship.getSpawnX() + 0.5,
                loaded.getOriginY() + ship.getSpawnY(),
                loaded.getOriginZ() + ship.getSpawnZ() + 0.5,
                player.getLocation().getYaw(),
                player.getLocation().getPitch());
        player.teleport(target);
        player.sendMessage(cfg.msgTpUp(ship.getName()));
        return true;
    }

    /**
     * Teleporte le joueur a sa position de retour. Si pas memorise,
     * tombe sur le sol au-dessous du ship.
     */
    public void teleportDown(Player player, Spaceship ship) {
        Location stored = returnLocations.remove(player.getUniqueId());
        if (stored != null && stored.getWorld() != null) {
            player.teleport(stored);
        } else {
            LoadedSpaceship loaded = db.getLoaded(ship.getId());
            if (loaded != null && loaded.getWorld() != null) {
                int x = loaded.getOriginX() + ship.getSizeX() / 2;
                int z = loaded.getOriginZ() + ship.getSizeZ() / 2;
                int y = loaded.getWorld().getHighestBlockYAt(x, z) + 1;
                player.teleport(new Location(loaded.getWorld(), x + 0.5, y, z + 0.5,
                        player.getLocation().getYaw(), player.getLocation().getPitch()));
            }
        }
        player.sendMessage(cfg.msgTpDown());
    }

    /**
     * True si le joueur se trouve actuellement DANS la bounding box du ship charge.
     */
    public boolean isPlayerOnShip(Player player, Spaceship ship) {
        LoadedSpaceship loaded = db.getLoaded(ship.getId());
        if (loaded == null) return false;
        return loaded.contains(player.getLocation());
    }

    /* ============================ DELETE ============================ */

    public void delete(Spaceship ship) {
        LoadedSpaceship loaded = db.getLoaded(ship.getId());
        if (loaded != null && loaded.getWorld() != null) {
            // Si le proprietaire est sur le ship, le tp au sol avant la disparition.
            OfflinePlayer off = Bukkit.getOfflinePlayer(ship.getOwnerUuid());
            if (off.isOnline()) {
                Player onlineOwner = off.getPlayer();
                if (onlineOwner != null && loaded.contains(onlineOwner.getLocation())) {
                    teleportDown(onlineOwner, ship);
                }
            }
            SchematicHelper.clearRegion(loaded.getWorld(),
                    loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                    ship.getSizeX(), ship.getSizeY(), ship.getSizeZ());
        }
        db.deleteSpaceship(ship.getId());
        // Supprime egalement la telecommande dans l'inventaire (si proprietaire en ligne).
        removeRemoteFromInventory(ship.getOwnerUuid(), ship.getId());
    }

    /** Supprime toutes les boussoles correspondant a shipId dans l'inventaire du joueur (s'il est en ligne). */
    public int removeRemoteFromInventory(UUID ownerUuid, long shipId) {
        Player p = Bukkit.getPlayer(ownerUuid);
        if (p == null) return 0;
        return removeRemotesFromInventory(p.getInventory(), shipId);
    }

    private int removeRemotesFromInventory(PlayerInventory inv, long shipId) {
        int removed = 0;
        RemoteManager rm = plugin.getRemoteManager();
        for (int i = 0; i < inv.getSize(); i++) {
            ItemStack it = inv.getItem(i);
            if (it == null || !rm.isRemote(it)) continue;
            Long id = it.getItemMeta().getPersistentDataContainer().get(rm.getShipIdKey(), PersistentDataType.LONG);
            if (id != null && id == shipId) {
                inv.setItem(i, null);
                removed++;
            }
        }
        return removed;
    }

    /**
     * Reconcilie l'inventaire du joueur :
     *   - supprime les telecommandes dont le ship n'existe plus,
     *   - supprime les doublons (>1 stack pour le meme shipId).
     * Renvoie le nombre d'items supprimes.
     */
    public int sanitizePlayerRemotes(Player player) {
        RemoteManager rm = plugin.getRemoteManager();
        PlayerInventory inv = player.getInventory();
        Map<Long, Integer> seen = new HashMap<>();
        int removed = 0;
        for (int i = 0; i < inv.getSize(); i++) {
            ItemStack it = inv.getItem(i);
            if (it == null || !rm.isRemote(it)) continue;
            Long id = it.getItemMeta().getPersistentDataContainer().get(rm.getShipIdKey(), PersistentDataType.LONG);
            if (id == null) { inv.setItem(i, null); removed++; continue; }
            if (db.getSpaceship(id) == null) {
                inv.setItem(i, null); removed++;
                continue;
            }
            Integer firstSlot = seen.put(id, i);
            if (firstSlot != null) {
                // Doublon : on retire l'item en cours et on garde le premier rencontre.
                inv.setItem(i, null);
                removed++;
            }
        }
        return removed;
    }

    /**
     * Decharge (capture + clear) tous les spaceships actuellement charges
     * appartenant au joueur. Utilise a la deconnexion.
     */
    public int unloadAllForPlayer(UUID ownerUuid) {
        int count = 0;
        for (LoadedSpaceship loaded : db.getLoadedByOwner(ownerUuid)) {
            Spaceship ship = db.getSpaceship(loaded.getSpaceshipId());
            if (ship == null) {
                db.markUnloaded(loaded.getSpaceshipId());
                continue;
            }
            World world = loaded.getWorld();
            if (world == null) {
                db.markUnloaded(loaded.getSpaceshipId());
                continue;
            }
            try {
                String updated = SchematicHelper.captureRegion(world,
                        loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                        ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(),
                        ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ());
                ship.setSchematicJson(updated);
                db.updateSpaceship(ship);
                SchematicHelper.clearRegion(world,
                        loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                        ship.getSizeX(), ship.getSizeY(), ship.getSizeZ());
                db.markUnloaded(loaded.getSpaceshipId());
                count++;
            } catch (Exception ex) {
                plugin.getLogger().warning("Erreur unloadAllForPlayer ship #" + ship.getId() + " : " + ex.getMessage());
            }
        }
        return count;
    }

    /* ============================ LEVEL / MONEY ============================ */

    /** Cout pour passer du level N a N+1 : upgrade-base * N. */
    public double upgradeCost(int currentLevel) {
        return Math.max(0, currentLevel) * cfg.costUpgradeBase();
    }

    /**
     * Tente d'ameliorer le ship d'un niveau (paye avec Vault par le proprietaire {@code payer}).
     * La taille effective grimpe selon la regle de progression mais le ship n'est physiquement
     * agrandi qu'au prochain summon (avec marquage des coins).
     * Renvoie true si l'achat est effectue.
     */
    public boolean upgradeLevel(Player payer, Spaceship ship) {
        if (ship.getLevel() >= ShipSizing.maxUsefulLevel(cfg)) return false;
        EconomyHook eco = plugin.getEconomy();
        if (!eco.isReady()) { payer.sendMessage(cfg.msgVaultUnavailable()); return false; }
        double cost = upgradeCost(ship.getLevel());
        if (!eco.has(payer.getUniqueId(), cost)) {
            payer.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(eco.getBalance(payer.getUniqueId()))));
            return false;
        }
        if (!eco.withdraw(payer.getUniqueId(), cost)) return false;
        ship.setLevel(ship.getLevel() + 1);
        db.updateSpaceship(ship);
        return true;
    }

    /* ============================ SAUT SPATIAL ============================ */

    /** Distance maximale d'un saut spatial pour le niveau donne. */
    public int spaceJumpMax(int level) {
        return Math.min(cfg.spaceJumpHardCap(), Math.max(cfg.spaceJumpMin(), cfg.spaceJumpBlocksPerLevel() * Math.max(1, level)));
    }

    /** Cout d'un saut spatial. */
    public double spaceJumpCost(int distance) {
        return Math.max(0, distance) * cfg.costSpaceJumpPerBlock();
    }

    /**
     * Effectue un saut spatial : decharge le ship a son emplacement courant,
     * le recharge a la nouvelle position (delta sur un axe), teleporte le joueur sur le ship.
     *
     * @param axis 'X', 'Y' ou 'Z'
     * @param signedDistance distance signee (negatif = direction opposee)
     */
    public boolean spaceJump(Player player, Spaceship ship, char axis, int signedDistance) {
        if (!ship.getOwnerUuid().equals(player.getUniqueId()) && !player.hasPermission("spaceship.admin")) {
            player.sendMessage(cfg.msgNotOwner());
            return false;
        }
        LoadedSpaceship loaded = db.getLoaded(ship.getId());
        if (loaded == null) { player.sendMessage(cfg.msgNotLoaded()); return false; }
        if (!isPlayerOnShip(player, ship)) { player.sendMessage(cfg.msgSpaceJumpNotOnShip()); return false; }

        int absDist = Math.abs(signedDistance);
        int max = spaceJumpMax(ship.getLevel());
        if (absDist < cfg.spaceJumpMin() || absDist > max) {
            player.sendMessage(cfg.msgSpaceJumpTooFar(max, ship.getLevel()));
            return false;
        }
        double cost = spaceJumpCost(absDist);
        EconomyHook eco = plugin.getEconomy();
        if (cost > 0) {
            if (!eco.isReady()) { player.sendMessage(cfg.msgVaultUnavailable()); return false; }
            if (!eco.has(player.getUniqueId(), cost)) {
                player.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(eco.getBalance(player.getUniqueId()))));
                return false;
            }
        }

        World world = loaded.getWorld();
        if (world == null) return false;

        // Calcule la nouvelle origine.
        int nx = loaded.getOriginX(), ny = loaded.getOriginY(), nz = loaded.getOriginZ();
        switch (Character.toUpperCase(axis)) {
            case 'X' -> nx += signedDistance;
            case 'Y' -> ny += signedDistance;
            case 'Z' -> nz += signedDistance;
            default -> { player.sendMessage(ConfigManager.color("&cAxe invalide.")); return false; }
        }
        ny = clampY(world, ny, ship.getSizeY());

        if (cfg.checkCollision() && SchematicHelper.hasCollision(world, nx, ny, nz,
                ship.getSizeX(), ship.getSizeY(), ship.getSizeZ())) {
            player.sendMessage(cfg.msgCollision());
            return false;
        }

        // 1) Capture l'etat actuel
        try {
            String updated = SchematicHelper.captureRegion(world,
                    loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                    ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(),
                    ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ());
            ship.setSchematicJson(updated);
        } catch (Exception ex) {
            player.sendMessage(ConfigManager.color("&cErreur capture : " + ex.getMessage()));
            return false;
        }
        // 2) Clear emplacement initial
        SchematicHelper.clearRegion(world, loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                ship.getSizeX(), ship.getSizeY(), ship.getSizeZ());
        // 3) Restore au nouvel emplacement
        try {
            SchematicHelper.restoreRegion(ship.getSchematicJson(), world, nx, ny, nz);
        } catch (Exception ex) {
            // Rollback : recharge a l'ancien emplacement
            try {
                SchematicHelper.restoreRegion(ship.getSchematicJson(), world,
                        loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ());
            } catch (Exception ignored) {}
            player.sendMessage(ConfigManager.color("&cErreur restauration : " + ex.getMessage()));
            return false;
        }

        db.updateSpaceship(ship);
        db.markLoaded(ship.getId(), world.getName(), nx, ny, nz);
        if (cost > 0) eco.withdraw(player.getUniqueId(), cost);

        // Teleporte le joueur sur le ship a son spawn.
        Location target = new Location(world,
                nx + ship.getSpawnX() + 0.5,
                ny + ship.getSpawnY(),
                nz + ship.getSpawnZ() + 0.5,
                player.getLocation().getYaw(), player.getLocation().getPitch());
        player.teleport(target);

        String dir = axisLabel(axis, signedDistance);
        player.sendMessage(cfg.msgSpaceJumpSuccess(absDist, dir, eco.isReady() ? eco.format(cost) : String.valueOf(cost)));
        return true;
    }

    private static String axisLabel(char axis, int signed) {
        return switch (Character.toUpperCase(axis)) {
            case 'X' -> signed >= 0 ? "Est (X+)"  : "Ouest (X-)";
            case 'Y' -> signed >= 0 ? "Haut (Y+)" : "Bas (Y-)";
            case 'Z' -> signed >= 0 ? "Sud (Z+)"  : "Nord (Z-)";
            default  -> "?";
        };
    }

    /* ============================ HELPERS ============================ */

    private int clampY(World world, int y, int sizeY) {
        int min = world.getMinHeight() + 1;
        int max = world.getMaxHeight() - sizeY - 1;
        return Math.max(min, Math.min(max, y));
    }

    /**
     * Retourne le LoadedSpaceship et le Spaceship si le joueur se tient sur l'un de SES ships.
     * Pratique pour les listeners de protection.
     */
    public Spaceship findOwnedShipPlayerIsOn(Player player) {
        List<Spaceship> ships = db.getSpaceshipsByOwner(player.getUniqueId());
        for (Spaceship s : ships) {
            if (isPlayerOnShip(player, s)) return s;
        }
        return null;
    }

    /**
     * Retourne le proprietaire du ship dont la bounding box contient cette position,
     * ou null sinon. Utilise par la protection de blocs.
     */
    public LoadedShipOwnerInfo findShipOwnerAt(World world, int x, int y, int z) {
        for (LoadedSpaceship loaded : db.getAllLoaded()) {
            if (!loaded.getWorldName().equals(world.getName())) continue;
            if (x < loaded.getOriginX() || x >= loaded.getOriginX() + loaded.getSizeX()) continue;
            if (y < loaded.getOriginY() || y >= loaded.getOriginY() + loaded.getSizeY()) continue;
            if (z < loaded.getOriginZ() || z >= loaded.getOriginZ() + loaded.getSizeZ()) continue;
            Spaceship ship = db.getSpaceship(loaded.getSpaceshipId());
            if (ship == null) continue;
            return new LoadedShipOwnerInfo(ship, loaded);
        }
        return null;
    }

    /** Petit conteneur pour la protection. */
    public record LoadedShipOwnerInfo(Spaceship ship, LoadedSpaceship loaded) {
        public UUID ownerUuid() { return ship.getOwnerUuid(); }
    }

    /**
     * Decharge tous les ships en base lors de la fermeture du serveur.
     * (Pour eviter qu'au prochain demarrage le ship soit considere comme charge alors qu'il a ete genere a un endroit qui sera reset).
     * On capture l'etat actuel pour ne rien perdre.
     */
    public void unloadAllOnShutdown() {
        for (LoadedSpaceship loaded : db.getAllLoaded()) {
            Spaceship ship = db.getSpaceship(loaded.getSpaceshipId());
            if (ship == null) {
                db.markUnloaded(loaded.getSpaceshipId());
                continue;
            }
            World world = loaded.getWorld();
            if (world == null) {
                db.markUnloaded(loaded.getSpaceshipId());
                continue;
            }
            try {
                String updated = SchematicHelper.captureRegion(world,
                        loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                        ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(),
                        ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ());
                ship.setSchematicJson(updated);
                db.updateSpaceship(ship);
                SchematicHelper.clearRegion(world,
                        loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(),
                        ship.getSizeX(), ship.getSizeY(), ship.getSizeZ());
            } catch (Exception ex) {
                plugin.getLogger().warning("Erreur shutdown ship #" + ship.getId() + " : " + ex.getMessage());
            }
            db.markUnloaded(loaded.getSpaceshipId());
        }
    }

    /**
     * Verifie en boucle si des joueurs sont tombes du ship et corrige eventuellement.
     * Pour l'instant ne fait rien, methode reservee a une evolution future.
     */
    public void tick() {
        // no-op
    }

    public DatabaseManager getDatabase() { return db; }

    /** Test pratique : Bukkit#getWorld. */
    @SuppressWarnings("unused")
    private static World worldOf(String name) { return Bukkit.getWorld(name); }
}
