import java.util.ArrayList;
import java.util.List;

import net.minecraft.server.MinecraftServer;

/**
 * Server.java - Interface to server stuff
 * 
 * @author James
 */
public class Server {

    private MinecraftServer server;

    /**
     * Creates a server
     * 
     * @param server
     */
    public Server(MinecraftServer server) {
        this.server = server;
    }

    /**
     * Sends a message to all users
     * 
     * @param msg
     *            Message text to send
     */
    public void messageAll(String msg) {
        server.f.a(new OPacket3Chat(msg));
    }

    /**
     * Bans specified player
     * 
     * @param player
     *            Name of the player to ban
     * 
     */
    public void ban(String player) {
        server.f.a(player);
    }

    /**
     * Unbans specified user
     * 
     * @param player
     *            Player name to unban
     * 
     */
    public void unban(String player) {
        server.f.b(player);
    }

    /**
     * Uses the specified console command
     * 
     * @param command
     */
    public void useConsoleCommand(String command) {
        server.a(command, server);
    }

    /**
     * Uses the specified console command
     * 
     * @param command
     *            command to use
     * @param player
     *            player to use command as
     */
    public void useConsoleCommand(String command, Player player) {
        server.a(command, player.getUser().a);
    }

    /**
     * Starts a timer using the built-in timer system.
     * 
     * @param uniqueString
     *            must be unique identifier for this timer
     * @param time
     *            time till it expires (6000 roughly equals 5 minutes)
     */
    public void setTimer(String uniqueString, int time) {
        MinecraftServer.b.put(uniqueString, time);
    }

    /**
     * Check to see if your timer has expired yet.
     * 
     * @param uniqueString
     *            unique identifier
     * @return false if timer has expired
     */
    public boolean isTimerExpired(String uniqueString) {
        return MinecraftServer.b.containsKey(uniqueString);
    }

    /**
     * Returns actual server time (-2^63 to 2^63-1)
     * 
     * @return time server time
     */
    public long getTime() {
        return server.e.k(); // Thank you Bukkit
    }

    /**
     * Returns current server time (0-24000)
     * 
     * @return time server time
     */
    public long getRelativeTime() {
        long time = (getTime() % 24000);
        // Java modulus is stupid.
        if (time < 0)
            time += 24000;
        return time;
    }

    /**
     * Sets the actual server time
     * 
     * @param time
     *            time (-2^63 to 2^63-1)
     */
    public void setTime(long time) {
        server.e.a(time); // Again, thank you Bukkit
    }

    /**
     * Sets the current server time
     * 
     * @param time
     *            time (0-24000)
     */
    public void setRelativeTime(long time) {
        long margin = (time - getTime()) % 24000;
        // Java modulus is stupid.
        if (margin < 0)
            margin += 24000;
        setTime(getTime() + margin);
    }

    /**
     * Returns the actual Minecraft Server
     * 
     * @return
     */
    public MinecraftServer getMCServer() {
        return server;
    }

    /**
     * Tries to match a character's name.
     * 
     * @param name
     * @return
     */
    public Player matchPlayer(String name) {
        Player lastPlayer = null;
        name = name.toLowerCase();

        for (Object player : server.f.b) {
            String playerName = ((OEntityPlayerMP) player).r;

            if (playerName.toLowerCase().equals(name)) {
                // Perfect match found
                lastPlayer = ((OEntityPlayerMP) player).getPlayer();
                break;
            }
            if (playerName.toLowerCase().indexOf(name.toLowerCase()) != -1) {
                // Partial match
                if (lastPlayer != null)
                    // Found multiple
                    return null;
                lastPlayer = ((OEntityPlayerMP) player).getPlayer();
            }
        }

        return lastPlayer;
    }

    /**
     * Returns specified player
     * 
     * @param name
     * @return
     */
    public Player getPlayer(String name) {
        OEntityPlayerMP user = server.f.i(name);
        return user == null ? null : user.getPlayer();
    }

    /**
     * Returns the player list.
     * 
     * @return list of players
     */
    public List<Player> getPlayerList() {
        List<Player> toRet = new ArrayList<Player>();
        for (Object o : server.f.b)
            toRet.add(((OEntityPlayerMP) o).getPlayer());
        return toRet;
    }

    /**
     * Returns the list of mobs in all open chunks.
     * 
     * @return list of mobs
     */
    public List<Mob> getMobList() {
        List<Mob> toRet = new ArrayList<Mob>();
        for (Object o : server.e.b)
            if (o instanceof OEntityMobs || o instanceof OEntityGhast)
                toRet.add(new Mob((OEntityLiving) o));
        return toRet;
    }

    /**
     * Returns the list of animals in all open chunks.
     * 
     * @return list of animals
     */
    public List<Mob> getAnimalList() {
        List<Mob> toRet = new ArrayList<Mob>();
        for (Object o : server.e.b)
            if (o instanceof OEntityAnimals)
                toRet.add(new Mob((OEntityLiving) o));
        return toRet;
    }

    /**
     * Returns the list of minecarts in all open chunks.
     * 
     * @return list of minecarts
     */
    public List<Minecart> getMinecartList() {
        List<Minecart> toRet = new ArrayList<Minecart>();
        for (Object o : server.e.b)
            if (o instanceof OEntityMinecart)
                toRet.add(((OEntityMinecart) o).cart);
        return toRet;
    }

    /**
     * Returns the list of boats in all open chunks.
     * 
     * @return list of boats
     */
    public List<Boat> getBoatList() {
        List<Boat> toRet = new ArrayList<Boat>();
        for (Object o : server.e.b)
            if (o instanceof OEntityBoat)
                toRet.add(((OEntityBoat) o).boat);
        return toRet;
    }

    /**
     * Returns the list of all entities in the server in open chunks.
     * 
     * @return list of entities
     */
    public List<BaseEntity> getEntityList() {
        List<BaseEntity> toRet = new ArrayList<BaseEntity>();
        for (Object o : server.e.b)
            if (o instanceof OEntityMobs || o instanceof OEntityGhast || o instanceof OEntityAnimals)
                toRet.add(new Mob((OEntityLiving) o));
            else if (o instanceof OEntityMinecart)
                toRet.add(((OEntityMinecart) o).cart);
            else if (o instanceof OEntityBoat)
                toRet.add(((OEntityBoat) o).boat);
            else if (o instanceof OEntityPlayerMP)
                toRet.add(((OEntityPlayerMP) o).getPlayer());
        return toRet;
    }

    /**
     * Returns the list of all living entities (players, animals, mobs) in open
     * chunks.
     * 
     * @return list of living entities
     */
    public List<LivingEntity> getLivingEntityList() {
        List<LivingEntity> toRet = new ArrayList<LivingEntity>();
        for (Object o : server.e.b)
            if (o instanceof OEntityMobs || o instanceof OEntityGhast || o instanceof OEntityAnimals)
                toRet.add(new Mob((OEntityLiving) o));
            else if (o instanceof OEntityPlayerMP)
                toRet.add(((OEntityPlayerMP) o).getPlayer());
        return toRet;
    }

    /**
     * Returns the list of vehicles in open chunks.
     * 
     * @return list of vehicles
     */
    public List<BaseVehicle> getVehicleEntityList() {
        List<BaseVehicle> toRet = new ArrayList<BaseVehicle>();
        for (Object o : server.e.b)
            if (o instanceof OEntityMinecart)
                toRet.add(((OEntityMinecart) o).cart);
            else if (o instanceof OEntityBoat)
                toRet.add(((OEntityBoat) o).boat);
        return toRet;
    }

    /**
     * Get the global spawn location
     * 
     * @return Location object for spawn
     */
    public Location getSpawnLocation() {
        // More structure ftw
        OWorldInfo info = server.e.q;
        Location spawn = new Location();
        spawn.x = (info.c() + 0.5D);
        spawn.y = server.e.e(info.c(), info.e()) + 1.5D;
        spawn.z = info.e() + 0.5D;
        spawn.rotX = 0.0F;
        spawn.rotY = 0.0F;
        return spawn;
    }

    /**
     * Sets the block
     * 
     * @param block
     * @return
     */
    public boolean setBlock(Block block) {
        return setBlockAt(block.getType(), block.getX(), block.getY(), block.getZ()) && setBlockData(block.getX(), block.getY(), block.getZ(), block.getData());
    }

    /**
     * Returns the block at the specified location
     * 
     * @param x
     * @param y
     * @param z
     * @return block
     */
    public Block getBlockAt(int x, int y, int z) {
        return new Block(getBlockIdAt(x, y, z), x, y, z, getBlockData(x, y, z));
    }

    /**
     * Returns the block data at the specified coordinates
     * 
     * @param x
     *            x
     * @param y
     *            y
     * @param z
     *            z
     * @return block data
     */
    public int getBlockData(int x, int y, int z) {
        return server.e.b(x, y, z);
    }

    /**
     * Sets the block data at the specified coordinates
     * 
     * @param x
     *            x
     * @param y
     *            y
     * @param z
     *            z
     * @param data
     *            data
     * @return true if it was successful
     */
    public boolean setBlockData(int x, int y, int z, int data) {
        boolean toRet = server.e.d(x, y, z, data);
        etc.getMCServer().f.a(new OPacket53BlockChange(x, y, z, etc.getMCServer().e));
        ComplexBlock block = getComplexBlock(x, y, z);
        if (block != null)
            block.update();
        return toRet;
    }

    /**
     * Sets the block type at the specified location
     * 
     * @param blockType
     * @param x
     * @param y
     * @param z
     * @return true if successful
     */
    public boolean setBlockAt(int blockType, int x, int y, int z) {
        return server.e.e(x, y, z, blockType);
    }

    /**
     * Returns the highest block Y
     * 
     * @param x
     * @param z
     * @return highest block altitude
     */
    public int getHighestBlockY(int x, int z) {
        return server.e.e(x, z);
    }

    /**
     * Returns the block type at the specified location
     * 
     * @param x
     * @param y
     * @param z
     * @return block type
     */
    public int getBlockIdAt(int x, int y, int z) {
        return server.e.a(x, y, z);
    }

    /**
     * Returns the complex block at the specified location. Null if there's no
     * complex block there. This will also find complex-blocks spanning multiple
     * spaces, such as double chests.
     * 
     * @param block
     * @return complex block
     */
    public ComplexBlock getComplexBlock(Block block) {
        return getComplexBlock(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Returns the complex block at the specified location. Null if there's no
     * complex block there. This will also find complex-blocks spanning multiple
     * spaces, such as double chests.
     * 
     * @param x
     *            x
     * @param y
     *            y
     * @param z
     *            z
     * @return complex block
     */
    public ComplexBlock getComplexBlock(int x, int y, int z) {
        ComplexBlock result = getOnlyComplexBlock(x, y, z);

        if (result != null)
            if (result instanceof Chest) {
                Chest chest = (Chest) result;
                result = chest.findAttachedChest();

                if (result != null)
                    return result;
                else
                    return chest;
            }

        return result;
    }

    /**
     * Returns the only complex block at the specified location. Null if there's
     * no complex block there.
     * 
     * @param block
     * @return complex block
     */
    public ComplexBlock getOnlyComplexBlock(Block block) {
        return getOnlyComplexBlock(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Returns the complex block at the specified location. Null if there's no
     * complex block there.
     * 
     * @param x
     *            x
     * @param y
     *            y
     * @param z
     *            z
     * @return complex block
     */
    public ComplexBlock getOnlyComplexBlock(int x, int y, int z) {
        OTileEntity localav = server.e.m(x, y, z);
        if (localav != null)
            if (localav instanceof OTileEntityChest)
                return new Chest((OTileEntityChest) localav);
            else if (localav instanceof OTileEntitySign)
                return new Sign((OTileEntitySign) localav);
            else if (localav instanceof OTileEntityFurnace)
                return new Furnace((OTileEntityFurnace) localav);
            else if (localav instanceof OTileEntityMobSpawner)
                return new MobSpawner((OTileEntityMobSpawner) localav);
            else if (localav instanceof OTileEntityDispenser)
                return new Dispenser((OTileEntityDispenser) localav);
        return null;
    }

    /**
     * Drops an item at the specified location
     * 
     * @param loc
     * @param itemId
     */
    public void dropItem(Location loc, int itemId) {
        dropItem(loc.x, loc.y, loc.z, itemId, 1);
    }

    /**
     * Drops an item at the specified location
     * 
     * @param x
     * @param y
     * @param z
     * @param itemId
     */
    public void dropItem(double x, double y, double z, int itemId) {
        dropItem(x, y, z, itemId, 1);
    }

    /**
     * Drops an item with desired quantity at the specified location
     * 
     * @param loc
     * @param itemId
     * @param quantity
     */
    public void dropItem(Location loc, int itemId, int quantity) {
        dropItem(loc.x, loc.y, loc.z, itemId, quantity);
    }

    /**
     * Drops an item with desired quantity at the specified location
     * 
     * @param x
     * @param y
     * @param z
     * @param itemId
     * @param quantity
     */
    public void dropItem(double x, double y, double z, int itemId, int quantity) {
        double d1 = server.e.k.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
        double d2 = server.e.k.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
        double d3 = server.e.k.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;

        OEntityItem localgl = new OEntityItem(server.e, x + d1, y + d2, z + d3, new OItemStack(itemId, quantity, 0));
        localgl.c = 10;
        server.e.a(localgl);
    }

    /**
     * Forces the server to update the physics for blocks around the given block
     * 
     * @param block
     *            the block that changed
     */
    public void updateBlockPhysics(Block block) {
        updateBlockPhysics(block.getX(), block.getY(), block.getZ(), block.getData());
    }

    /**
     * Forces the server to update the physics for blocks around the given block
     * 
     * @param x
     *            the X coordinate of the block
     * @param y
     *            the Y coordinate of the block
     * @param z
     *            the Z coordinate of the block
     * @param data
     *            the new data for the block
     */
    public void updateBlockPhysics(int x, int y, int z, int data) {
        server.e.c(x, y, z, data);
    }

    /**
     * Adds a runnable to the Server Queue, so that it will be executed in the
     * Server Thread.
     * 
     * @param r
     *            - the runnable
     */
    public void addToServerQueue(Runnable r) {
        addToServerQueue(r, 0L);
    }

    /**
     * Executes a runnable in the server thread after a specified delay.
     * 
     * @param r
     *            - the runnable
     * @param delayMillis
     *            - the delay in milliseconds
     */
    public void addToServerQueue(Runnable r, long delayMillis) {
        OEntityTracker.add(r, delayMillis);
    }

    /**
     * Saves all player inventories to file
     */
    public void saveInventories() {
        server.f.d();
    }

    /**
     * Checks to see whether or not the chunk containing the given block is
     * loaded into memory.
     * 
     * @param block
     *            the Block to check
     * @return true if the chunk is loaded
     */
    public boolean isChunkLoaded(Block block) {
        return isChunkLoaded(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Checks to see whether or not the chunk containing the given block
     * coordinates is loaded into memory.
     * 
     * @param x
     *            a block x-coordinate
     * @param y
     *            a block y-coordinate
     * @param z
     *            a block z-coordinate
     * @return true if the chunk is loaded
     */
    public boolean isChunkLoaded(int x, int y, int z) {
        return server.e.u.a(x >> 4, z >> 4);
    }

    /**
     * Loads the chunk containing the given block. If the chunk does not exist,
     * it will be generated.
     * 
     * @param block
     *            the Block to check
     */
    public void loadChunk(Block block) {
        loadChunk(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Loads the chunk containing the given block coordinates. If the chunk does
     * not exist, it will be generated.
     * 
     * @param x
     *            a block x-coordinate
     * @param y
     *            a block y-coordinate
     * @param z
     *            a block z-coordinate
     */
    public void loadChunk(int x, int y, int z) {
        loadChunk(x >> 4, z >> 4);
    }

    /**
     * Loads the chunk containing the given chunk coordinates. If the chunk does
     * not exist, it will be generated.
     * 
     * @param x
     *            a chunk x-coordinate
     * @param z
     *            a chunk z-coordinate
     */
    public void loadChunk(int x, int z) {
        server.e.u.d(x, z);
    }

    /**
     * Checks if the provided block is being powered through redstone
     * 
     * @param block
     *            Block to check
     * @return true if the block is being powered
     */
    public boolean isBlockPowered(Block block) {
        return isBlockPowered(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Checks if the provided block is being powered through redstone
     * 
     * @param x
     *            a block x-coordinate
     * @param y
     *            a block y-coordinate
     * @param z
     *            a block z-coordinate
     * @return true if the block is being powered
     */
    public boolean isBlockPowered(int x, int y, int z) {
        return server.e.o(x, y, z);
    }

    /**
     * Checks if the provided block is being indirectly powered through redstone
     * 
     * @param block
     *            Block to check
     * @return true if the block is being indirectly powered
     */
    public boolean isBlockIndirectlyPowered(Block block) {
        return isBlockIndirectlyPowered(block.getX(), block.getY(), block.getZ());
    }

    /**
     * Checks if the provided block is being indirectly powered through redstone
     * 
     * @param x
     *            a block x-coordinate
     * @param y
     *            a block y-coordinate
     * @param z
     *            a block z-coordinate
     * @return true if the block is being indirectly powered
     */
    public boolean isBlockIndirectlyPowered(int x, int y, int z) {
        return server.e.o(x, y, z);
    }
}
