protected void getInheritedPermissions(
      String worldName,
      List<String> permissions,
      boolean groupInheritance,
      boolean worldInheritance) {
    permissions.addAll(Arrays.asList(this.getTimedPermissions(worldName)));
    permissions.addAll(Arrays.asList(this.getOwnPermissions(worldName)));

    if (worldName != null) {
      // World inheritance
      for (String parentWorld : this.manager.getWorldInheritance(worldName)) {
        getInheritedPermissions(parentWorld, permissions, false, true);
      }

      // Common permissions
      if (!worldInheritance) { // skip common world permissions if we are inside world-inheritance
        // tree
        getInheritedPermissions(null, permissions, false, true);
      }
    }

    // Group inhertance
    if (groupInheritance) {
      for (PermissionGroup parentGroup : this.getGroups(worldName)) {
        parentGroup.getInheritedPermissions(
            worldName, permissions, true, false, new HashSet<PermissionGroup>());
      }
    }

    // Add all child nodes
    for (String node : permissions.toArray(new String[0])) {
      this.getInheritedChildPermissions(node, permissions);
    }
  }
  /**
   * Set parent groups for user
   *
   * @param groups array of parent group objects
   */
  public void setGroups(PermissionGroup[] parentGroups, String worldName) {
    List<String> groups = new LinkedList<String>();

    for (PermissionGroup group : parentGroups) {
      groups.add(group.getName());
    }

    this.setGroups(groups.toArray(new String[0]), worldName);
  }
  /**
   * Get group names in specified world
   *
   * @return String array of user's group names
   */
  public String[] getGroupsNames(String worldName) {
    List<String> groups = new LinkedList<String>();
    for (PermissionGroup group : this.getGroups(worldName)) {
      if (group != null) {
        groups.add(group.getName());
      }
    }

    return groups.toArray(new String[0]);
  }
  /**
   * Return all ladders the user is participating in
   *
   * @return Map, key - name of ladder, group - corresponding group of that ladder
   */
  public Map<String, PermissionGroup> getRankLadders() {
    Map<String, PermissionGroup> ladders = new HashMap<String, PermissionGroup>();

    for (PermissionGroup group : this.getParents()) {
      if (!group.isRanked()) {
        continue;
      }

      ladders.put(group.getRankLadder(), group);
    }

    return ladders;
  }
  /**
   * Check if this user is member of group or one of its descendant groups (optionally)
   *
   * @param group group as PermissionGroup object
   * @param worldName
   * @param checkInheritance if true then descendant groups of the given group would be checked too
   * @return true on success, false otherwise
   */
  public boolean inGroup(PermissionGroup group, String worldName, boolean checkInheritance) {
    for (PermissionGroup parentGroup : this.getParents(worldName)) {
      if (parentGroup.equals(group)) {
        return true;
      }

      if (checkInheritance && parentGroup.isChildOf(group, worldName, true)) {
        return true;
      }
    }

    return false;
  }
  protected boolean checkMembership(PermissionGroup group, String worldName) {
    int groupLifetime =
        this.getOwnOptionInteger("group-" + group.getName() + "-until", worldName, 0);

    if (groupLifetime > 0
        && groupLifetime < System.currentTimeMillis() / 1000) { // check for expiration
      this.setOption("group-" + group.getName() + "-until", null, worldName); // remove option
      this.removeGroup(group, worldName); // remove membership
      // @TODO Make notification of player about expired memebership
      return false;
    }

    return true;
  }
  /**
   * Remove user from group
   *
   * @param group group as PermissionGroup object
   */
  public void removeGroup(PermissionGroup group, String worldName) {
    if (group == null) {
      return;
    }

    this.removeGroup(group.getName(), worldName);
  }
  @Override
  public String getOption(String optionName, String worldName, String defaultValue) {
    String cacheIndex = worldName + "|" + optionName;

    if (this.cachedOptions.containsKey(cacheIndex)) {
      return this.cachedOptions.get(cacheIndex);
    }

    String value = this.getOwnOption(optionName, worldName, null);
    if (value != null) {
      this.cachedOptions.put(cacheIndex, value);
      return value;
    }

    if (worldName != null) { // world inheritance
      for (String world : manager.getWorldInheritance(worldName)) {
        value = this.getOption(optionName, world, null);
        if (value != null) {
          this.cachedOptions.put(cacheIndex, value);
          return value;
        }
      }

      // Check common space
      value = this.getOption(optionName, null, null);
      if (value != null) {
        this.cachedOptions.put(cacheIndex, value);
        return value;
      }
    }

    // Inheritance
    for (PermissionGroup group : this.getGroups(worldName)) {
      value = group.getOption(optionName, worldName, null);
      if (value != null) {
        this.cachedOptions.put(cacheIndex, value); // put into cache inherited value
        return value;
      }
    }

    // Nothing found
    return defaultValue;
  }
  /**
   * Promotes user in specified ladder. If user is not member of the ladder RankingException will be
   * thrown If promoter is not null and he is member of the ladder and his rank is lower then user's
   * RankingException will be thrown too. If there is no group to promote the user to
   * RankingException would be thrown
   *
   * @param promoter null if action is performed from console or by a plugin
   * @param ladderName Ladder name
   * @throws RankingException
   */
  public PermissionGroup promote(PermissionUser promoter, String ladderName)
      throws RankingException {
    if (ladderName == null || ladderName.isEmpty()) {
      ladderName = "default";
    }

    int promoterRank = getPromoterRankAndCheck(promoter, ladderName);
    int rank = this.getRank(ladderName);

    PermissionGroup sourceGroup = this.getRankLadders().get(ladderName);
    PermissionGroup targetGroup = null;

    for (Map.Entry<Integer, PermissionGroup> entry :
        this.manager.getRankLadder(ladderName).entrySet()) {
      int groupRank = entry.getValue().getRank();
      if (groupRank >= rank) { // group have equal or lower than current rank
        continue;
      }

      if (groupRank <= promoterRank) { // group have higher rank than promoter
        continue;
      }

      if (targetGroup != null
          && groupRank <= targetGroup.getRank()) { // group have higher rank than target group
        continue;
      }

      targetGroup = entry.getValue();
    }

    if (targetGroup == null) {
      throw new RankingException("User are not promoteable", this, promoter);
    }

    this.swapGroups(sourceGroup, targetGroup);

    this.callEvent(PermissionEntityEvent.Action.RANK_CHANGED);

    return targetGroup;
  }
  @Override
  public String getPrefix(String worldName) {
    // @TODO This method should be refactored

    if (!this.cachedPrefix.containsKey(worldName)) {
      String localPrefix = this.getOwnPrefix(worldName);

      if (worldName != null && (localPrefix == null || localPrefix.isEmpty())) {
        // World-inheritance
        for (String parentWorld : this.manager.getWorldInheritance(worldName)) {
          String prefix = this.getOwnPrefix(parentWorld);
          if (prefix != null && !prefix.isEmpty()) {
            localPrefix = prefix;
            break;
          }
        }

        // Common space
        if (localPrefix == null || localPrefix.isEmpty()) {
          localPrefix = this.getOwnPrefix(null);
        }
      }

      if (localPrefix == null || localPrefix.isEmpty()) {
        for (PermissionGroup group : this.getGroups(worldName)) {
          localPrefix = group.getPrefix(worldName);
          if (localPrefix != null && !localPrefix.isEmpty()) {
            break;
          }
        }
      }

      if (localPrefix == null) { // just for NPE safety
        localPrefix = "";
      }

      this.cachedPrefix.put(worldName, localPrefix);
    }

    return this.cachedPrefix.get(worldName);
  }