/** @author andune */
@Singleton
public class EssentialsModule
    implements com.andune.minecraft.hsp.integration.Essentials, Initializable {
  private static final Logger log = LoggerFactory.getLogger(EssentialsModule.class);

  private Plugin essentialsPlugin;
  private Map<String, List<PluginCommand>> altcommands;
  private final Plugin plugin;
  private final BukkitCommandRegister bukkitCommandRegister;
  private final Scheduler scheduler;

  @Inject
  public EssentialsModule(
      Plugin bukkitPlugin, BukkitCommandRegister bukkitCommandRegister, Scheduler scheduler) {
    this.plugin = bukkitPlugin;
    this.bukkitCommandRegister = bukkitCommandRegister;
    this.scheduler = scheduler;
  }

  /**
   * Called to register HSP's commands with Essentials, which then enables Essentials "respectful"
   * command usurp behavior which will let HSP own commands like "/home" and "/spawn".
   */
  private void registerCommands() {
    log.debug("entering registerCommands()");

    essentialsPlugin = plugin.getServer().getPluginManager().getPlugin("Essentials");

    if (essentialsPlugin == null) {
      log.debug("Essentials plugin not found, registerComamnds() doing nothing");
      return;
    }

    try {
      grabInternalAltCommands();
      mapHSPCommands();
    } catch (Exception e) {
      log.error("Caught exception when trying to register commands with Essentials", e);
    }

    log.debug("exiting registerCommands()");
  }

  /**
   * Using Essentials own internal alternate commands map, assign HSP commands into the map. This is
   * a replication of the internal algorithm that Essentials uses in AlternativeCommandsHandler as
   * of Essentials 2.10.1.
   */
  private void mapHSPCommands() {
    Map<String, PluginCommand> hspCommands = bukkitCommandRegister.getLoadedCommands();
    Collection<PluginCommand> commands = hspCommands.values();
    //        final String pluginName = plugin.getDescription().getName().toLowerCase();

    log.debug("commands.size() = {}", commands == null ? null : commands.size());
    for (Command command : commands) {
      final PluginCommand pc = (PluginCommand) command;
      final List<String> labels = new ArrayList<String>(pc.getAliases());
      labels.add(pc.getName());

      log.debug("registering command {}", pc.getName());
      //            PluginCommand reg = plugin.getServer().getPluginCommand(pluginName + ":" +
      // pc.getName().toLowerCase());
      //            if (reg == null)
      //            {
      //                reg = plugin.getServer().getPluginCommand(pc.getName().toLowerCase());
      //            }
      //            if (reg == null || !reg.getPlugin().equals(plugin))
      //            {
      //                continue;
      //            }
      //            log.debug("reg = {}", reg);
      for (String label : labels) {
        log.debug("registering label {}", label);
        List<PluginCommand> plugincommands = altcommands.get(label.toLowerCase());
        if (plugincommands == null) {
          plugincommands = new ArrayList<PluginCommand>();
          altcommands.put(label.toLowerCase(), plugincommands);
        }
        boolean found = false;
        for (PluginCommand pc2 : plugincommands) {
          if (pc2.getPlugin().equals(plugin)) {
            found = true;
          }
        }
        if (!found) {
          plugincommands.add(pc);
        }
      }
    }
  }

  /**
   * Method to grab Essentials internal altCommands hash. This is ugly code but Essentials offers no
   * external APIs to make this any cleaner.
   *
   * @throws NoSuchFieldException
   * @throws SecurityException
   * @throws IllegalAccessException
   * @throws IllegalArgumentException
   */
  @SuppressWarnings("unchecked")
  private void grabInternalAltCommands()
      throws SecurityException, NoSuchFieldException, IllegalArgumentException,
          IllegalAccessException {
    Essentials pluginObject = (Essentials) essentialsPlugin;
    Field commandHandler = pluginObject.getClass().getDeclaredField("alternativeCommandsHandler");
    commandHandler.setAccessible(true);
    AlternativeCommandsHandler ach = (AlternativeCommandsHandler) commandHandler.get(pluginObject);
    Field altCommandsField = ach.getClass().getDeclaredField("altcommands");
    altCommandsField.setAccessible(true);
    altcommands = (HashMap<String, List<PluginCommand>>) altCommandsField.get(ach);

    log.debug("altcommands = {}", altcommands);
  }

  @Override
  public void init() throws Exception {
    // we register commands a few ticks after the server has started
    // up. This gives Essentials time to load (if present).
    scheduler.scheduleSyncDelayedTask(
        new Runnable() {
          public void run() {
            registerCommands();
          }
        },
        3);
  }

  @Override
  public void shutdown() throws Exception {}

  @Override
  public int getInitPriority() {
    return 9;
  }

  @Override
  public boolean isEnabled() {
    return essentialsPlugin != null;
  }

  @Override
  public String getVersion() {
    if (essentialsPlugin != null) return essentialsPlugin.getDescription().getVersion();
    else return null;
  }
}
/**
 * Implementation of ChunkStorage that uses the file system.
 *
 * <p>This is for TESTING ONLY. It is expected that on a production server, thousands of chunk files
 * will cause I/O delays, the same as the original MC server did when it had one-file-per-chunk. A
 * solution is to group files into regions like MC does now, so if someone wants to contribute such
 * an implementation, it could be used on production servers.
 *
 * @author andune
 */
public class FileChunkStorage implements ChunkStorage {
  private final Logger log = LoggerFactory.getLogger(FileChunkStorage.class);
  private final File worldContainer;

  public FileChunkStorage(Plugin plugin) {
    this.worldContainer = plugin.getServer().getWorldContainer();
  }

  /**
   * Return the region directory and create it if it doesn't exist.
   *
   * @param worldName
   * @param chunkX
   * @param chunkZ
   * @return
   */
  private File getRegionDirectory(String worldName, int chunkX, int chunkZ) {
    int regionX = chunkX >> 4;
    int regionZ = chunkZ >> 4;

    File regionDirectory =
        new File(worldContainer, worldName + "/blockOwner/" + regionX + "_" + regionZ);
    if (!regionDirectory.exists()) {
      regionDirectory.mkdirs();
    }

    return regionDirectory;
  }

  @Override
  public void save(Chunk chunk) throws IOException {
    File regionDirectory = getRegionDirectory(chunk.world, chunk.x, chunk.z);
    File f = new File(regionDirectory, chunk.x + "_" + chunk.z);
    DataOutputStream os = null;

    try {
      os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
      os.writeInt(chunk.map.assigned);

      final int[] keys = chunk.map.keys;
      final short[] values = chunk.map.values;
      final boolean[] states = chunk.map.allocated;

      for (int i = 0; i < states.length; i++) {
        if (states[i]) {
          os.writeInt(keys[i]);
          os.writeShort(values[i]);
        }
      }

      os.flush();
    } finally {
      if (os != null) os.close();
    }
  }

  @Override
  public void load(Chunk chunk) throws IOException {
    File regionDirectory = getRegionDirectory(chunk.world, chunk.x, chunk.z);
    File f = new File(regionDirectory, chunk.x + "_" + chunk.z);

    // if no file exists, just initialize a small-capacity map
    if (!f.exists()) {
      log.debug("load on chunk {}, no file exists, initializing empty dataset", chunk);
      chunk.map = new IntShortOpenHashMap(100);
      return;
    }

    DataInputStream is = null;

    try {
      is = new DataInputStream(new BufferedInputStream(new FileInputStream(f)));

      // first 32 bits represent the size of the map
      int available = is.readInt();

      // we round up by 30% to account for .75 load factor in hash.
      // this initializes the map at less than the load factor with
      // some extra room for growth before needing a clone & grow
      chunk.map = new IntShortOpenHashMap((int) (available * 1.3));

      while (is.available() > 0) {
        int key = is.readInt();
        short value = is.readShort();
        chunk.map.put(key, value);
        if (log.isDebugEnabled()) {
          Formatter format = new Formatter();
          format.format("loaded chunk{%d,%d} owner %d for key %x", chunk.x, chunk.z, value, key);
          log.debug(format.toString());
          format.close();
        }
      }
    } finally {
      if (is != null) is.close();
    }
  }
}
/** @author andune */
public class HomeInviteDAOEBean implements HomeInviteDAO {
  private static final Logger log = LoggerFactory.getLogger(HomeInviteDAOEBean.class);
  protected static final String TABLE = "hsp_homeinvite";

  private EbeanServer ebean;
  private final Storage storage;
  private final ConfigCore configCore;
  private final EbeanStorageUtil util;

  public HomeInviteDAOEBean(
      final EbeanServer ebean,
      final Storage storage,
      ConfigCore configCore,
      final EbeanStorageUtil util) {
    setEbeanServer(ebean);
    this.storage = storage;
    this.configCore = configCore;
    this.util = util;
  }

  public void setEbeanServer(final EbeanServer ebean) {
    this.ebean = ebean;
  }

  @Override
  public HomeInvite findHomeInviteById(int id) {
    String q = "find homeInvite where id = :id";

    Query<HomeInvite> query = ebean.createQuery(HomeInvite.class, q);
    query.setParameter("id", id);

    return query.findUnique();
  }

  @Override
  public HomeInvite findInviteByHomeAndInvitee(Home home, String invitee) {
    String q;
    if (configCore.useEbeanSearchLower())
      q = "find homeInvite where home = :home and lower(invitedPlayer) = lower(:invitee)";
    else q = "find homeInvite where home = :home and invitedPlayer = :invitee";

    Query<HomeInvite> query = ebean.createQuery(HomeInvite.class, q);
    query.setParameter("home", home.getId());
    query.setParameter("invitee", invitee);

    return query.findUnique();
  }

  @Override
  public Set<HomeInvite> findInvitesByHome(Home home) {
    String q = "find homeInvite where home = :home";
    Query<HomeInvite> query = ebean.createQuery(HomeInvite.class, q);
    query.setParameter("home", home.getId());

    return query.findSet();
  }

  @Override
  public Set<HomeInvite> findAllAvailableInvites(String invitee) {
    String q;
    if (configCore.useEbeanSearchLower())
      q = "find homeInvite where lower(invitedPlayer) = lower(:invitee)";
    else q = "find homeInvite where invitedPlayer = :invitee";

    Query<HomeInvite> query = ebean.createQuery(HomeInvite.class, q);
    query.setParameter("invitee", invitee);

    return query.findSet();
  }

  @Override
  public Set<HomeInvite> findAllPublicInvites() {
    return findAllAvailableInvites(HomeInvite.PUBLIC_HOME);
  }

  @Override
  public Set<HomeInvite> findAllOpenInvites(String player) {
    Set<HomeInvite> invites = new HashSet<HomeInvite>(5);

    // first find all homes for this player
    Set<? extends Home> homes = storage.getHomeDAO().findHomesByPlayer(player);
    if (homes == null || homes.size() == 0) return invites;

    // then find all HomeInvites related to any of those homes
    for (Home home : homes) {
      Set<HomeInvite> homeInvites = findInvitesByHome(home);
      if (homeInvites != null) invites.addAll(homeInvites);
    }

    return invites;
  }

  @Override
  public void saveHomeInvite(HomeInvite homeInvite) {
    Transaction tx = ebean.beginTransaction();
    ebean.save(homeInvite, tx);
    tx.commit();
  }

  @Override
  public void deleteHomeInvite(HomeInvite homeInvite) throws StorageException {
    Transaction tx = ebean.beginTransaction();
    tx.setPersistCascade(false);
    ebean.delete(homeInvite, tx);
    tx.commit();
  }

  @Override
  public Set<HomeInvite> findAllHomeInvites() {
    return ebean.find(HomeInvite.class).findSet();
  }

  @Override
  public int purgePlayerData(long purgeTime) {
    return util.purgePlayers(this, purgeTime);
  }

  @Override
  public int purgeWorldData(final String world) {
    int rowsPurged = 0;

    // delete any invites whose home is on the purged world
    Set<HomeInvite> set = findAllHomeInvites();
    for (HomeInvite hi : set) {
      if (world.equals(hi.getHome().getWorld())) {
        try {
          deleteHomeInvite(hi);
          rowsPurged++;
        } catch (Exception e) {
          log.error("Caught exception while purging homeInvite", e);
        }
      }
    }
    return rowsPurged;
  }

  @Override
  public int purgePlayer(String playerName) {
    // first delete any homeInvites the player was invited too
    int rowsPurged = util.deleteRows(TABLE, "invited_player", playerName);

    // then delete any HomeInvites the player sent out to others
    Set<HomeInvite> set = findAllOpenInvites(playerName);
    for (HomeInvite hi : set) {
      try {
        deleteHomeInvite(hi);
        rowsPurged++;
      } catch (Exception e) {
        log.error("Caught exception while purging homeInvite", e);
      }
    }
    return rowsPurged;
  }

  @Override
  public Set<String> getAllPlayerNames() {
    Set<HomeInvite> set = findAllHomeInvites();
    Set<String> playerNames = new HashSet<String>(set.size() * 3 / 2);
    for (HomeInvite hi : set) {
      playerNames.add(hi.getInvitedPlayer());
      playerNames.add(hi.getHome().getPlayerName());
    }
    return playerNames;
  }
}