/*
 *
 * PlaceholderAPI
 * Copyright (C) 2019 Ryan McCarthy
 *
 * This program 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.
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 */
package me.clip.placeholderapi;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import me.clip.placeholderapi.events.ExpansionRegisterEvent;
import me.clip.placeholderapi.events.ExpansionUnregisterEvent;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.clip.placeholderapi.expansion.Relational;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static me.clip.placeholderapi.util.Msg.color;

public class PlaceholderAPI {
  
  private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("[%]([^%]+)[%]");
  private static final Pattern BRACKET_PLACEHOLDER_PATTERN = Pattern.compile("[{]([^{}]+)[}]");
  private static final Pattern RELATIONAL_PLACEHOLDER_PATTERN = Pattern.compile("[%](rel_)([^%]+)[%]");
  private static final Map<String, PlaceholderHook> placeholders = new HashMap<>();
  
  private PlaceholderAPI() {
  }
  
  /**
   * Check if a specific placeholder identifier is currently registered
   *
   * @param identifier The identifier to check
   * @return true if identifier is already registered
   */
  public static boolean isRegistered(String identifier) {
    return getRegisteredIdentifiers().stream().filter(id -> id.equalsIgnoreCase(identifier))
            .findFirst().orElse(null) != null;
  }
  
  /**
   * Register a new placeholder hook
   *
   * @param identifier Identifier of the placeholder -> "%(identifier)_(args...)%
   * @param placeholderHook Implementing class that contains the onPlaceholderRequest method which
   * is called when a value is needed for the specific placeholder
   * @return true if the hook was successfully registered, false if there is already a hook
   * registered for the specified identifier
   */
  public static boolean registerPlaceholderHook(String identifier, PlaceholderHook placeholderHook) {
    Validate.notNull(identifier, "Identifier can not be null");
    Validate.notNull(placeholderHook, "Placeholderhook can not be null");
    
    if (isRegistered(identifier)) {
      return false;
    }
    
    placeholders.put(identifier.toLowerCase(), placeholderHook);
    
    return true;
  }
  
  /**
   * Unregister a placeholder hook by identifier
   *
   * @param identifier The identifier for the placeholder hook to unregister
   * @return true if the placeholder hook was successfully unregistered, false if there was no
   * placeholder hook registered for the identifier specified
   */
  public static boolean unregisterPlaceholderHook(String identifier) {
    Validate.notNull(identifier, "Identifier can not be null");
    return placeholders.remove(identifier.toLowerCase()) != null;
  }
  
  /**
   * Get all registered placeholder identifiers
   *
   * @return All registered placeholder identifiers
   */
  public static Set<String> getRegisteredIdentifiers() {
    return ImmutableSet.copyOf(placeholders.keySet());
  }
  
  /**
   * Get map of registered placeholders
   *
   * @return Copy of the internal placeholder map
   */
  public static Map<String, PlaceholderHook> getPlaceholders() {
    return ImmutableMap.copyOf(placeholders);
  }
  
  public static Set<PlaceholderExpansion> getExpansions() {
    Set<PlaceholderExpansion> set = getPlaceholders().values().stream()
            .filter(PlaceholderExpansion.class::isInstance).map(PlaceholderExpansion.class::cast)
            .collect(Collectors.toCollection(HashSet::new));
    
    return ImmutableSet.copyOf(set);
  }
  
  /**
   * Check if a String contains any PlaceholderAPI placeholders ({@literal %<identifier>_<params>%}).
   *
   * @param text String to check
   * @return true if String contains any registered placeholder identifiers, false otherwise
   */
  public static boolean containsPlaceholders(String text) {
    return text != null && PLACEHOLDER_PATTERN.matcher(text).find();
  }
  
  /**
   * Check if a String contains any PlaceholderAPI bracket placeholders ({@literal {<identifier>_<params>}}).
   *
   * @param text String to check
   * @return true if String contains any registered placeholder identifiers, false otherwise
   */
  public static boolean containsBracketPlaceholders(String text) {
    return text != null && BRACKET_PLACEHOLDER_PATTERN.matcher(text).find();
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @return String containing all translated placeholders
   */
  public static List<String> setBracketPlaceholders(OfflinePlayer player, List<String> text) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, true);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return String containing all translated placeholders
   */
  public static List<String> setBracketPlaceholders(OfflinePlayer player, List<String> text, boolean colorize) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, colorize);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @return String containing all translated placeholders
   */
  public static List<String> setPlaceholders(OfflinePlayer player, List<String> text) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, true);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return String containing all translated placeholders
   */
  public static List<String> setPlaceholders(OfflinePlayer player, List<String> text, boolean colorize) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, colorize);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>You set the pattern yourself through this method.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @param pattern The pattern to match placeholders to. Capture group 1 must contain an underscore separating the 
   * identifier from the params
   * @return String containing all translated placeholders
   */
  public static List<String> setPlaceholders(OfflinePlayer player, List<String> text, Pattern pattern) {
    return setPlaceholders(player, text, pattern, true);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>You set the pattern yourself through this method.
   *
   * @param player Player to parse the placeholders against
   * @param text List of Strings to set the placeholder values in
   * @param pattern The pattern to match placeholders to. Capture group 1 must contain an underscore separating the 
   * identifier from the params
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return String containing all translated placeholders
   */
  public static List<String> setPlaceholders(OfflinePlayer player, List<String> text, Pattern pattern, boolean colorize) {
    if(text == null) {
      return null;
    }
    
    return text.stream().map(line -> setPlaceholders(player, line, pattern, colorize))
            .collect(Collectors.toList());
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
   *
   * @param player Player to parse the placeholders against
   * @param text Text to set the placeholder values in
   * @return String containing all translated placeholders
   */
  public static String setBracketPlaceholders(OfflinePlayer player, String text) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, true);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}
   *
   * @param player Player to parse the placeholders against
   * @param text Text to set the placeholder values in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return String containing all translated placeholders
   */
  public static String setBracketPlaceholders(OfflinePlayer player, String text, boolean colorize) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, colorize);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
   *
   * @param player Player to parse the placeholders against
   * @param text Text to set the placeholder values in
   * @return String containing all translated placeholders
   */
  public static String setPlaceholders(OfflinePlayer player, String text) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
   *
   * @param player Player to parse the placeholder against
   * @param text Text to parse the placeholders in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return The text containing the parsed placeholders
   */
  public static String setPlaceholders(OfflinePlayer player, String text, boolean colorize) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, colorize);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>You set the pattern yourself through this method.
   *
   * @param player Player to parse the placeholders against
   * @param text Text to set the placeholder values in
   * @param pattern The pattern to match placeholders to. Capture group 1 must contain an underscore separating the 
   * identifier from the params
   * @return The text containing the parsed placeholders
   */
  public static String setPlaceholders(OfflinePlayer player, String text, Pattern pattern) {
    return setPlaceholders(player, text, pattern, true);
  }
  
  /**
   * Translates all placeholders into their corresponding values.
   * <br>You set the pattern yourself through this method.
   *
   * @param player Player to parse the placeholders against
   * @param text Text to set the placeholder values in
   * @param pattern The pattern to match placeholders to. Capture group 1 must contain an underscore separating the 
   * identifier from the params
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return The text containing the parsed placeholders
   */
  public static String setPlaceholders(OfflinePlayer player, String text, Pattern pattern, boolean colorize) {
    if (text == null) {
      return null;
    }
    
    if (placeholders.isEmpty()) {
      return colorize ? color(text) : text;
    }
    
    Matcher m = pattern.matcher(text);
    Map<String, PlaceholderHook> hooks = getPlaceholders();
    
    while (m.find()) {
      String format = m.group(1);
      int index = format.indexOf("_");
      
      if (index <= 0 || index >= format.length()) {
        continue;
      }
      
      String identifier = format.substring(0, index).toLowerCase();
      String params = format.substring(index + 1);
      
      if (hooks.containsKey(identifier)) {
        String value = hooks.get(identifier).onRequest(player, params);
        if (value != null) {
          text = text.replaceAll(Pattern.quote(m.group()), Matcher.quoteReplacement(value));
        }
      }
    }
    
    return colorize ? color(text) : text;
  }
  
  /**
   * Translate placeholders in the provided List based on the relation of the two provided players.
   * <br>The pattern of a valid placeholder is {@literal %rel_<identifier>_<param>%}.
   *
   * @param one Player to compare
   * @param two Player to compare
   * @param text text to parse the placeholder values to
   * @return The text containing the parsed relational placeholders
   */
  public static List<String> setRelationalPlaceholders(Player one, Player two, List<String> text) {
    return setRelationalPlaceholders(one, two, text, true);
  }
  
  /**
   * Translate placeholders in the provided list based on the relation of the two provided players.
   * <br>The pattern of a valid placeholder is {@literal %rel_<identifier>_<params>%}.
   *
   * @param one First player to compare
   * @param two Second player to compare
   * @param text Text to parse the placeholders in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return The text containing the parsed relational placeholders
   */
  public static List<String> setRelationalPlaceholders(Player one, Player two, List<String> text, boolean colorize) {
    if(text == null) {
      return null;
    }
    
    return text.stream().map(line -> setRelationalPlaceholders(one, two, line, colorize))
            .collect(Collectors.toList());
  }
  
  /**
   * set relational placeholders in the text specified placeholders are matched with the pattern
   * %<rel_(identifier)_(params)>% when set with this method
   *
   * @param one First player to compare
   * @param two Second player to compare
   * @param text Text to parse the placeholders in
   * @return The text containing the parsed relational placeholders
   */
  public static String setRelationalPlaceholders(Player one, Player two, String text) {
    return setRelationalPlaceholders(one,two, text, true);
  }
  
  /**
   * set relational placeholders in the text specified placeholders are matched with the pattern
   * %<rel_(identifier)_(params)>% when set with this method
   *
   * @param one Player to compare
   * @param two Player to compare
   * @param text Text to parse the placeholders in
   * @param colorize If color codes (&[0-1a-fk-o]) should be translated
   * @return The text containing the parsed relational placeholders
   */
  public static String setRelationalPlaceholders(Player one, Player two, String text, boolean colorize) {
    if (text == null) {
      return null;
    }
    
    if (placeholders.isEmpty()) {
      return colorize ? color(text) : text;
    }
    
    Matcher m = RELATIONAL_PLACEHOLDER_PATTERN.matcher(text);
    Map<String, PlaceholderHook> hooks = getPlaceholders();
    
    while (m.find()) {
      String format = m.group(2);
      int index = format.indexOf("_");
      
      if (index <= 0 || index >= format.length()) {
        continue;
      }
      
      String identifier = format.substring(0, index).toLowerCase();
      String params = format.substring(index + 1);
      
      if (hooks.containsKey(identifier)) {
        if (!(hooks.get(identifier) instanceof Relational)) {
          continue;
        }
        
        Relational rel = (Relational) hooks.get(identifier);
        String value = rel.onPlaceholderRequest(one, two, params);
        
        if (value != null) {
          text = text.replaceAll(Pattern.quote(m.group()), Matcher.quoteReplacement(value));
        }
      }
    }
    
    return colorize ? color(text) : text;
  }
  
  /**
   * Unregister ALL placeholder hooks that are currently registered
   */
  protected static void unregisterAll() {
    unregisterAllProvidedExpansions();
    placeholders.clear();
  }
  
  /**
   * Unregister all expansions provided by PlaceholderAPI
   */
  public static void unregisterAllProvidedExpansions() {
    if (placeholders.isEmpty()) {
      return;
    }
    
    getPlaceholders().forEach((key, value) -> {
      if (value instanceof PlaceholderExpansion) {
        PlaceholderExpansion ex = (PlaceholderExpansion) value;
        
        if (!ex.persist()) {
          unregisterExpansion(ex);
        }
      }
    });
  }
  
  public static boolean registerExpansion(PlaceholderExpansion ex) {
    ExpansionRegisterEvent ev = new ExpansionRegisterEvent(ex);
    Bukkit.getPluginManager().callEvent(ev);
    if (ev.isCancelled()) {
      return false;
    }
    
    return registerPlaceholderHook(ex.getIdentifier(), ex);
  }
  
  public static boolean unregisterExpansion(PlaceholderExpansion ex) {
    if (unregisterPlaceholderHook(ex.getIdentifier())) {
      Bukkit.getPluginManager().callEvent(new ExpansionUnregisterEvent(ex));
      return true;
    }
    
    return false;
  }
  
  /**
   * Gets the placeholder pattern for the default placeholders.
   *
   * @return The pattern for {@literal %<identifier>_<params>%}
   */
  public static Pattern getPlaceholderPattern() {
    return PLACEHOLDER_PATTERN;
  }
  
  /**
   * Gets the placeholder pattern for the bracket placeholders.
   *
   * @return The pattern for {@literal {<identifier>_<params>}}
   */
  public static Pattern getBracketPlaceholderPattern() {
    return BRACKET_PLACEHOLDER_PATTERN;
  }
  
  /**
   * Gets the placeholder pattern for the relational placeholders.
   *
   * @return The pattern for {@literal %rel_<identifier>_<params>%}
   */
  public static Pattern getRelationalPlaceholderPattern() {
    return RELATIONAL_PLACEHOLDER_PATTERN;
  }
  
  @Deprecated
  public static Set<String> getRegisteredPlaceholderPlugins() {
    return getRegisteredIdentifiers();
  }
  
  @Deprecated
  public static Set<String> getExternalPlaceholderPlugins() {
    return null;
  }
  
  @Deprecated
  public static boolean registerPlaceholderHook(Plugin plugin, PlaceholderHook placeholderHook) {
    return plugin != null && registerPlaceholderHook(plugin.getName(), placeholderHook);
  }
  
  @Deprecated
  public static boolean unregisterPlaceholderHook(Plugin plugin) {
    return plugin != null && unregisterPlaceholderHook(plugin.getName());
  }
  
  public static String setPlaceholders(Player player, String text) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, true);
  }
  
  public static String setPlaceholders(Player player, String text, boolean colorize) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, colorize);
  }
  
  public static List<String> setPlaceholders(Player player, List<String> text) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, true);
  }
  
  public static List<String> setPlaceholders(Player player, List<String> text, boolean colorize) {
    return setPlaceholders(player, text, PLACEHOLDER_PATTERN, colorize);
  }
  
  public static String setBracketPlaceholders(Player player, String text) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, true);
  }
  
  public static String setBracketPlaceholders(Player player, String text, boolean colorize) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, colorize);
  }
  
  public static List<String> setBracketPlaceholders(Player player, List<String> text) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, true);
  }
  
  public static List<String> setBracketPlaceholders(Player player, List<String> text, boolean colorize) {
    return setPlaceholders(player, text, BRACKET_PLACEHOLDER_PATTERN, colorize);
  }
}