/**
 * TerraCraftTools(SuperSoapTools) - MineModule.java
 * Copyright (c) 2013 Jeremy Koletar (jjkoletar), <http://jj.koletar.com>
 * Copyright (c) 2013 computerdude5000,<computerdude5000@gmail.com>
 *
 * TerraCraftTools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TerraCraftTools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TerraCraftTools.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.github.computerdude5000.terracrafttools.modules;

import com.github.computerdude5000.terracrafttools.TerraCraftTools;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBucketEmptyEvent;

import java.io.File;
import java.io.FilenameFilter;
import java.util.*;

/**
 * MineModule
 * <p/>
 * Module to automatically or manually reset mines in the jail.
 *
 * @author jjkoletar
 */
public class MineModule implements SuperSoapToolsModule, Listener {
    private TerraCraftTools plugin = null;
    private List<Mine> mines = null;

    private static class IsYML implements FilenameFilter {
        public boolean accept(File file, String name) {
            return name.contains(".mine.yml");
        }
    }

    /**
     * initModule enables the module called by {@link TerraCraftTools}
     */
    public void initModule(TerraCraftTools sst) {
        mines = new LinkedList<Mine>();
        plugin = sst;
        plugin.commandRegistrar.registerCommand("resetmine", this);
        plugin.commandRegistrar.registerCommand("listmines", this);
        plugin.commandRegistrar.registerCommand("checkmine", this);
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
        File[] mineFiles = plugin.getDataFolder().listFiles(new IsYML());
        if (mineFiles.length == 0) {
            return;
        }
        for (File mine : mineFiles) {
            FileConfiguration mineConf = YamlConfiguration
                    .loadConfiguration(mine);
            String mineKey = mineConf.getString("name");
            int minX = (Integer) mineConf.get("minX");
            int minY = (Integer) mineConf.get("minY");
            int minZ = (Integer) mineConf.get("minZ");
            int maxX = (Integer) mineConf.get("maxX");
            int maxY = (Integer) mineConf.get("maxY");
            int maxZ = (Integer) mineConf.get("maxZ");
            int surface = (Integer) mineConf.get("surface");
            String world = mineConf.getString("world");
            String composition = mineConf.getString("composition");
            String[] blocks = composition.split("\\;");
            Map<Integer, Double> comp = new HashMap<Integer, Double>();
            for (String block : blocks) {
                String[] blockComp = block.split("\\|");
                comp.put(Integer.parseInt(blockComp[0]),
                        Double.valueOf(blockComp[1]));
            }
            List<MineResetCriteria> rc = new LinkedList<MineResetCriteria>();
            String resetString = mineConf.getString("reset");
            String[] resets = resetString.split("\\;");
            for (String reset : resets) {
                if (reset.contains(">")) {
                    String[] bits = reset.split("\\>");
                    rc.add(new MineResetCriteria(Double.valueOf(bits[1]), '>',
                            Integer.valueOf(bits[0])));
                } else if (reset.contains("<")) {
                    String[] bits = reset.split("\\<");
                    rc.add(new MineResetCriteria(Double.valueOf(bits[1]), '<',
                            Integer.valueOf(bits[0])));
                }
            }
            mines.add(new Mine(minX, minY, minZ, maxX, maxY, maxZ, plugin
                    .getServer().getWorld(world), mineKey, rc, comp, surface));
        }
        plugin.getServer()
                .getScheduler()
                .scheduleSyncRepeatingTask(
                        plugin,
                        new CheckMineTask(this),
                        plugin.getModuleConfig("Mine").getInt("interval") * 60 * 20,
                        plugin.getModuleConfig("Mine").getInt("interval") * 60 * 20);
        plugin.logger
                .info("[TerraCraftTools][MineModule] MineModule Module Enabled!");
    }

    /**
     * deinitModule disables the module called by {@link TerraCraftTools}
     */
    public void deinitModule() {
        plugin.commandRegistrar.unregisterCommand("resetmine", this);
        plugin.commandRegistrar.unregisterCommand("checkmine", this);
        plugin.commandRegistrar.unregisterCommand("listmines", this);
        HandlerList.unregisterAll(this);
        mines = null;
        plugin.logger
                .info("[TerraCraftTools][MineModule] MineModule Module Disabled!");
        plugin = null;
    }

    /**
     * Returns true or false when a player types a command
     *
     * @param sender  sender who sent the message
     * @param command command that was sent
     * @param label   idk what this does.
     * @param args    arguments that were sent along with the command
     * @return true or false
     */
    public boolean callCommand(CommandSender sender, Command command,
                               String label, String[] args) {
        if (!sender.isOp()) {
            return false;
        }
        if (command.getName().equalsIgnoreCase("resetmine")) {
            // find matching mines
            StringBuilder sb = new StringBuilder();
            for (String s : args) {
                sb.append(s);
                sb.append(" ");
            }
            sb.deleteCharAt(sb.length() - 1);
            String query = sb.toString();
            List<Mine> matchedMines = new LinkedList<Mine>();
            for (Mine mine : mines) {
                if (mine.getName().toLowerCase().contains(query.toLowerCase())) {
                    matchedMines.add(mine);
                }
            }
            if (matchedMines.size() == 0) {
                sender.sendMessage(ChatColor.RED + "No mines matched!");
                return false;
            } else if (matchedMines.size() > 1) {
                sender.sendMessage(ChatColor.RED
                        + "Too many mines matched, be more specific.");
                return false;
            }
            plugin.getServer()
                    .getScheduler()
                    .scheduleSyncDelayedTask(
                            plugin,
                            new ResetMineDelay(matchedMines.get(0), sender
                                    .getName()), 60 * 20);
            Bukkit.getServer().broadcastMessage(
                    ChatColor.WHITE + "[" + ChatColor.DARK_RED
                            + matchedMines.get(0).getName() + " Mine"
                            + ChatColor.WHITE + "] " + ChatColor.GREEN
                            + matchedMines.get(0).getName()
                            + " Mine will reset in 1 minute!");
        } else if (command.getName().equalsIgnoreCase("listmines")) {
            for (Mine mine : mines) {
                sender.sendMessage(ChatColor.LIGHT_PURPLE + mine.getName());
            }

        } else if (command.getName().equalsIgnoreCase("checkmine")) {
            // find matching mines
            StringBuilder sb = new StringBuilder();
            for (String s : args) {
                sb.append(s);
                sb.append(" ");
            }
            sb.deleteCharAt(sb.length() - 1);
            String query = sb.toString();
            List<Mine> matchedMines = new LinkedList<Mine>();
            for (Mine mine : mines) {
                if (mine.getName().toLowerCase().contains(query.toLowerCase())) {
                    matchedMines.add(mine);
                }
            }
            if (matchedMines.size() == 0) {
                sender.sendMessage(ChatColor.RED + "No mines matched!");
                return false;
            } else if (matchedMines.size() > 1) {
                sender.sendMessage(ChatColor.RED
                        + "Too many mines matched, be more specific.");
                return false;
            }
            sender.sendMessage(ChatColor.LIGHT_PURPLE
                    + matchedMines.get(0).getName()
                    + " "
                    + (matchedMines.get(0).shouldMineReset(sender) ? ChatColor.GREEN
                    + "should"
                    : ChatColor.RED + "shouldn't")
                    + ChatColor.LIGHT_PURPLE + " reset.");
        }
        return true;
    }

    /**
     * Callback for async task to check the mines automatically.
     */
    protected void checkAllMines() {
        plugin.logger
                .info("[TerraCraftTools][MineModule] Checking mines for reset qualities...");
        for (Mine m : mines) {
            if (m.shouldMineReset(null)) {
                Bukkit.getServer()
                        .getScheduler()
                        .scheduleSyncDelayedTask(plugin,
                                new ResetMineDelay(m, ""), 60 * 20);
                Bukkit.getServer().broadcastMessage(
                        ChatColor.WHITE + "[" + ChatColor.DARK_RED
                                + m.getName() + " Mine" + ChatColor.WHITE
                                + "] " + ChatColor.GREEN + m.getName()
                                + " Mine will reset in 1 minute!");
            }
        }
    }

    /**
     * Mine
     * <p/>
     * Represents a single mine, containing it's location bounds, reset
     * criteria, and composition.
     *
     * @author jjkoletar
     */
    private static class Mine {
        private int minX;
        private int minY;
        private int minZ;
        private int maxX;
        private int maxY;
        private int maxZ;
        private String name;
        private World world;
        private List<MineResetCriteria> criteria;
        private List<CompositionElement> composition;
        private int surface;

        public Mine(int mnX, int mnY, int mnZ, int mxX, int mxY, int mxZ,
                    World w, String n, List<MineResetCriteria> cri,
                    Map<Integer, Double> c, int surface) {
            minX = mnX;
            minY = mnY;
            minZ = mnZ;
            maxX = mxX;
            maxY = mxY;
            maxZ = mxZ;
            world = w;
            name = n;
            criteria = cri;
            this.surface = surface;
            // parse composition into probability table
            composition = new ArrayList<CompositionElement>();
            double max = 0;
            for (Map.Entry<Integer, Double> entry : c.entrySet()) {
                max += entry.getValue();
            }
            double i = 0;
            for (Map.Entry<Integer, Double> entry : c.entrySet()) {
                double v = entry.getValue() / max;
                i += v;
                composition.add(new CompositionElement(entry.getKey(), i));
            }
        }

        private static class CompositionElement {
            private int block;
            private double chance;

            public CompositionElement(int i, double d) {
                block = i;
                chance = d;
            }

            public int getBlock() {
                return block;
            }

            public double getChance() {
                return chance;
            }
        }

        /**
         * Reset a mine based off random chance from its composition.
         */
        public void resetMine() {
            // Pull players out
            for (Player p : Bukkit.getServer().getOnlinePlayers()) {
                Location l = p.getLocation();
                if ((l.getX() >= minX && l.getX() <= maxX)
                        && (l.getY() >= minY && l.getY() <= maxY)
                        && (l.getZ() >= minZ && l.getZ() <= maxZ)) {
                    p.teleport(new Location(world, l.getX(), maxY + 2D, l
                            .getZ()));
                }
            }
            Random rand = new Random(world.getTime());
            for (int x = minX; x <= maxX; ++x) {
                for (int z = minZ; z <= maxZ; ++z) {
                    for (int y = minY; y <= maxY; ++y) {
                        if (y == maxY) {
                            world.getBlockAt(x, y, z).setTypeId(surface);
                            continue;
                        }
                        double r = rand.nextDouble();
                        for (CompositionElement entry : composition) {
                            if (r <= entry.getChance()) {
                                world.getBlockAt(x, y, z).setTypeId(
                                        entry.getBlock());
                                break;
                            }
                        }
                    }
                }
            }
        }

        /**
         * Get the name of the mine
         *
         * @return Mine name
         */
        public String getName() {
            return name;
        }

        /**
         * Check mine to see if it should be reset.
         *
         * @param sender Command sender to receive status data back, <i>null</i> if
         *               automatic execution.
         * @return <i>true</i> when >=1 criteria matched, <i>false</i> when zero
         *         criteria matched.
         */
        public boolean shouldMineReset(CommandSender sender) {
            Map<Integer, Integer> blocks = new HashMap<Integer, Integer>();
            double blocksSeen = 0;
            for (int x = minX; x <= maxX; ++x) {
                for (int y = minY; y <= maxY; ++y) {
                    for (int z = minZ; z <= maxZ; ++z) {
                        blocksSeen = blocksSeen + 1;
                        if (blocks.containsKey(world.getBlockAt(x, y, z)
                                .getTypeId())) {
                            blocks.put(world.getBlockAt(x, y, z).getTypeId(),
                                    blocks.get(world.getBlockAt(x, y, z)
                                            .getTypeId()) + 1);
                        } else if (!blocks.containsKey(world
                                .getBlockAt(x, y, z).getTypeId())) {
                            blocks.put(world.getBlockAt(x, y, z).getTypeId(), 1);
                        }
                    }
                }
            }
            Map<Integer, Double> percentageComposition = new HashMap<Integer, Double>();
            for (Map.Entry<Integer, Integer> entry : blocks.entrySet()) {
                percentageComposition.put(entry.getKey(), entry.getValue()
                        / blocksSeen);
            }
            boolean resettable = false;
            for (MineResetCriteria m : criteria) {
                if (percentageComposition.containsKey(m.getBlockId())) {
                    if (m.isCriteriaMatched(percentageComposition.get(m
                            .getBlockId()))) {
                        resettable = true;
                    }
                } else {
                    // We have a criterion with 0% of the block in the mine.
                    // Gasp! X-Rayers!
                    resettable = true;
                }
            }
            if (sender != null) {
                for (Map.Entry<Integer, Double> entry : percentageComposition
                        .entrySet()) {
                    sender.sendMessage(ChatColor.LIGHT_PURPLE + "Block "
                            + entry.getKey() + ": " + entry.getValue() * 100
                            + "%");
                }
            }
            return resettable;
        }

        public boolean isLocationContained(Location l) {
            return (l.getX() >= minX && l.getX() <= maxX)
                    && (l.getY() >= minY && l.getY() <= maxY)
                    && (l.getZ() >= minZ && l.getZ() <= maxZ);
        }
    }

    /**
     * Delay task for resetting a mine, usually a 1 minute delay.
     *
     * @author jjkoletar
     */
    private static class ResetMineDelay implements Runnable {
        private Mine mine = null;
        private String who = "";

        public ResetMineDelay(Mine m, String s) {
            mine = m;
            who = s;
        }

        public void run() {
            mine.resetMine();
            Bukkit.getServer()
                    .broadcastMessage(
                            ChatColor.WHITE
                                    + "["
                                    + ChatColor.DARK_RED
                                    + mine.getName()
                                    + " Mine"
                                    + ChatColor.WHITE
                                    + "] "
                                    + ChatColor.GREEN
                                    + mine.getName()
                                    + " Mine has been reset"
                                    + (who.equals("") ? " automatically"
                                    : " by " + who) + "!");
        }
    }

    /**
     * Task for automatic checking of mine compositions
     *
     * @author jjkoletar
     */
    private static class CheckMineTask implements Runnable {
        private MineModule module;

        public CheckMineTask(MineModule m) {
            module = m;
        }

        public void run() {
            module.checkAllMines();
        }
    }

    /**
     * Class representing a criteria that can be matched
     *
     * @author jjkoletar
     */
    private static class MineResetCriteria {
        private int blockId;
        private double compare;
        private char compareRoutine;

        /**
         * @param c     'Percentage' (0.0-1.0) to be greater than or less than of
         *              a single block
         * @param r     <i>></i> for a block's distrubution to be greater than
         *              <i>c</i>, <i><</i> for a block's distrubution to be less
         *              than <i>c</i>.
         * @param block Integer block ID
         */
        public MineResetCriteria(double c, char r, int block) {
            compareRoutine = r;
            compare = c;
            blockId = block;
        }

        /**
         * @return Integer block ID
         */
        public int getBlockId() {
            return blockId;
        }

        /**
         * Compare the criteria's values to <i>d</i>.
         *
         * @param d Value calculated in the {@link Mine} class.
         * @return <i>true</i> if the criteria was matched, <i>false</i> if not.
         */
        public boolean isCriteriaMatched(double d) {
            if (compareRoutine == '>') {
                return d > compare;
            } else if (compareRoutine == '<') {
                return d < compare;
            }
            return false;
        }
    }

    @EventHandler
    public void onBucketEmpty(PlayerBucketEmptyEvent event) {
        if (event.getBucket().equals(Material.LAVA_BUCKET)) {
            Block b = event.getBlockClicked().getRelative(event.getBlockFace());
            for (Mine mine : mines) {
                if (mine.isLocationContained(b.getLocation())) {
                    event.setCancelled(true);
                    return;
                }
            }
        }
    }
}
