package com.earth2me.essentials.textreader;

import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.ExecuteTimer;
import com.earth2me.essentials.PlayerList;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.DescParseTickFormat;
import com.earth2me.essentials.utils.EnumUtil;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.NumberUtil;
import net.ess3.api.IEssentials;
import org.bukkit.Location;
import org.bukkit.Statistic;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;

import java.lang.management.ManagementFactory;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.earth2me.essentials.I18n.tl;

//When adding a keyword here, you also need to add the implementation above
enum KeywordType {
    PLAYER(KeywordCachable.CACHEABLE),
    DISPLAYNAME(KeywordCachable.CACHEABLE),
    USERNAME(KeywordCachable.NOTCACHEABLE),
    NICKNAME(KeywordCachable.CACHEABLE),
    PREFIX(KeywordCachable.CACHEABLE),
    SUFFIX(KeywordCachable.CACHEABLE),
    GROUP(KeywordCachable.CACHEABLE),
    BALANCE(KeywordCachable.CACHEABLE),
    MAILS(KeywordCachable.CACHEABLE),
    PLAYTIME(KeywordCachable.CACHEABLE),
    WORLD(KeywordCachable.CACHEABLE),
    WORLDNAME(KeywordCachable.CACHEABLE),
    ONLINE(KeywordCachable.CACHEABLE),
    UNIQUE(KeywordCachable.CACHEABLE),
    WORLDS(KeywordCachable.CACHEABLE),
    PLAYERLIST(KeywordCachable.SUBVALUE, true),
    TIME(KeywordCachable.CACHEABLE),
    DATE(KeywordCachable.CACHEABLE),
    WORLDTIME12(KeywordCachable.CACHEABLE),
    WORLDTIME24(KeywordCachable.CACHEABLE),
    WORLDDATE(KeywordCachable.CACHEABLE),
    COORDS(KeywordCachable.CACHEABLE),
    TPS(KeywordCachable.CACHEABLE),
    UPTIME(KeywordCachable.CACHEABLE),
    IP(KeywordCachable.CACHEABLE, true),
    ADDRESS(KeywordCachable.CACHEABLE, true),
    PLUGINS(KeywordCachable.CACHEABLE, true),
    VERSION(KeywordCachable.CACHEABLE, true);
    private final KeywordCachable type;
    private final boolean isPrivate;

    KeywordType(final KeywordCachable type) {
        this.type = type;
        this.isPrivate = false;
    }

    KeywordType(final KeywordCachable type, final boolean isPrivate) {
        this.type = type;
        this.isPrivate = isPrivate;
    }

    public KeywordCachable getType() {
        return type;
    }

    public boolean isPrivate() {
        return isPrivate;
    }
}

enum KeywordCachable {
    CACHEABLE, // This keyword can be cached as a string
    SUBVALUE, // This keyword can be cached as a map
    NOTCACHEABLE // This keyword should never be cached
}

public class KeywordReplacer implements IText {
    private static final Statistic PLAY_ONE_TICK = EnumUtil.getStatistic("PLAY_ONE_MINUTE", "PLAY_ONE_TICK");
    private static final Pattern KEYWORD = Pattern.compile("\\{([^\\{\\}]+)\\}");
    private static final Pattern KEYWORDSPLIT = Pattern.compile("\\:");
    private final transient IText input;
    private final transient List<String> replaced;
    private final transient IEssentials ess;
    private final transient boolean includePrivate;
    private final transient boolean replaceSpacesWithUnderscores;
    private final EnumMap<KeywordType, Object> keywordCache = new EnumMap<>(KeywordType.class);

    public KeywordReplacer(final IText input, final CommandSource sender, final IEssentials ess) {
        this.input = input;
        this.replaced = new ArrayList<>(this.input.getLines().size());
        this.ess = ess;
        this.includePrivate = true;
        this.replaceSpacesWithUnderscores = false;
        replaceKeywords(sender);
    }

    public KeywordReplacer(final IText input, final CommandSource sender, final IEssentials ess, final boolean showPrivate) {
        this.input = input;
        this.replaced = new ArrayList<>(this.input.getLines().size());
        this.ess = ess;
        this.includePrivate = showPrivate;
        this.replaceSpacesWithUnderscores = false;
        replaceKeywords(sender);
    }

    public KeywordReplacer(final IText input, final CommandSource sender, final IEssentials ess, final boolean showPrivate,
                           final boolean replaceSpacesWithUnderscores) {
        this.input = input;
        this.replaced = new ArrayList<>(this.input.getLines().size());
        this.ess = ess;
        this.includePrivate = showPrivate;
        this.replaceSpacesWithUnderscores = replaceSpacesWithUnderscores;
        replaceKeywords(sender);
    }

    private void replaceKeywords(final CommandSource sender) {
        final ExecuteTimer execTimer = new ExecuteTimer();
        execTimer.start();
        User user = null;
        if (sender.isPlayer()) {
            user = ess.getUser(sender.getPlayer());
        }
        execTimer.mark("User Grab");

        for (int i = 0; i < input.getLines().size(); i++) {
            String line = input.getLines().get(i);
            final Matcher matcher = KEYWORD.matcher(line);

            while (matcher.find()) {
                final String fullMatch = matcher.group(0);
                final String keywordMatch = matcher.group(1);
                final String[] matchTokens = KEYWORDSPLIT.split(keywordMatch);
                line = replaceLine(line, fullMatch, matchTokens, user);
            }
            replaced.add(line);
        }

        execTimer.mark("Text Replace");
        final String timeroutput = execTimer.end();
        if (ess.getSettings().isDebug()) {
            ess.getLogger().log(Level.INFO, "Keyword Replacer " + timeroutput);
        }
    }

    private String replaceLine(String line, final String fullMatch, final String[] matchTokens, final User user) {
        final String keyword = matchTokens[0];
        try {
            String replacer = null;
            final KeywordType validKeyword = KeywordType.valueOf(keyword);
            if (validKeyword.getType().equals(KeywordCachable.CACHEABLE) && keywordCache.containsKey(validKeyword)) {
                replacer = keywordCache.get(validKeyword).toString();
            } else if (validKeyword.getType().equals(KeywordCachable.SUBVALUE)) {
                String subKeyword = "";
                if (matchTokens.length > 1) {
                    subKeyword = matchTokens[1].toLowerCase(Locale.ENGLISH);
                }

                if (keywordCache.containsKey(validKeyword)) {
                    final Map<String, String> values = (Map<String, String>) keywordCache.get(validKeyword);
                    if (values.containsKey(subKeyword)) {
                        replacer = values.get(subKeyword);
                    }
                }
            }

            if (validKeyword.isPrivate() && !includePrivate) {
                replacer = "";
            }

            if (replacer == null) {
                replacer = "";
                switch (validKeyword) {
                    case PLAYER:
                    case DISPLAYNAME:
                        if (user != null) {
                            replacer = user.getDisplayName();
                        }
                        break;
                    case USERNAME:
                        if (user != null) {
                            replacer = user.getName();
                        }
                        break;
                    case NICKNAME:
                        if (user != null) {
                            final String nickname = user.getFormattedNickname();
                            replacer = nickname == null ? user.getName() : nickname;
                        }
                        break;
                    case PREFIX:
                        if (user != null) {
                            final String prefix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getPrefix(user.getBase()));
                            replacer = prefix == null ? "" : prefix;
                        }
                        break;
                    case SUFFIX:
                        if (user != null) {
                            final String suffix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getSuffix(user.getBase()));
                            replacer = suffix == null ? "" : suffix;
                        }
                        break;
                    case GROUP:
                        if (user != null) {
                            replacer = user.getGroup();
                        }
                        break;
                    case BALANCE:
                        if (user != null) {
                            replacer = NumberUtil.displayCurrency(user.getMoney(), ess);
                        }
                        break;
                    case MAILS:
                        if (user != null) {
                            replacer = Integer.toString(user.getMails().size());
                        }
                        break;
                    case PLAYTIME:
                        if (user != null) {
                            final long playtimeMs = System.currentTimeMillis() - (user.getBase().getStatistic(PLAY_ONE_TICK) * 50L);
                            replacer = DateUtil.formatDateDiff(playtimeMs);
                        }
                        break;
                    case WORLD:
                    case WORLDNAME:
                        if (user != null) {
                            final Location location = user.getLocation();
                            replacer = location == null || location.getWorld() == null ? "" : location.getWorld().getName();
                        }
                        break;
                    case ONLINE:
                        int playerHidden = 0;
                        for (final User u : ess.getOnlineUsers()) {
                            if (u.isHidden()) {
                                playerHidden++;
                            }
                        }
                        replacer = Integer.toString(ess.getOnlinePlayers().size() - playerHidden);
                        break;
                    case UNIQUE:
                        replacer = NumberFormat.getInstance().format(ess.getUserMap().getUniqueUsers());
                        break;
                    case WORLDS:
                        final StringBuilder worldsBuilder = new StringBuilder();
                        for (final World w : ess.getServer().getWorlds()) {
                            if (worldsBuilder.length() > 0) {
                                worldsBuilder.append(", ");
                            }
                            worldsBuilder.append(w.getName());
                        }
                        replacer = worldsBuilder.toString();
                        break;
                    case PLAYERLIST:
                        final Map<String, String> outputList;
                        if (keywordCache.containsKey(validKeyword)) {
                            outputList = (Map<String, String>) keywordCache.get(validKeyword);
                        } else {
                            final boolean showHidden;
                            if (user == null) {
                                showHidden = true;
                            } else {
                                showHidden = user.isAuthorized("essentials.list.hidden") || user.canInteractVanished();
                            }

                            //First lets build the per group playerlist
                            final Map<String, List<User>> playerList = PlayerList.getPlayerLists(ess, user, showHidden);
                            outputList = new HashMap<>();
                            for (final String groupName : playerList.keySet()) {
                                final List<User> groupUsers = playerList.get(groupName);
                                if (groupUsers != null && !groupUsers.isEmpty()) {
                                    outputList.put(groupName, PlayerList.listUsers(ess, groupUsers, " "));
                                }
                            }

                            //Now lets build the all user playerlist
                            final StringBuilder playerlistBuilder = new StringBuilder();
                            for (final Player p : ess.getOnlinePlayers()) {
                                if (ess.getUser(p).isHidden()) {
                                    continue;
                                }
                                if (playerlistBuilder.length() > 0) {
                                    playerlistBuilder.append(", ");
                                }
                                playerlistBuilder.append(p.getDisplayName());
                            }
                            outputList.put("", playerlistBuilder.toString());
                            keywordCache.put(validKeyword, outputList);
                        }

                        //Now thats all done, output the one we want and cache the rest.
                        if (matchTokens.length == 1) {
                            replacer = outputList.get("");
                        } else if (outputList.containsKey(matchTokens[1].toLowerCase(Locale.ENGLISH))) {
                            replacer = outputList.get(matchTokens[1].toLowerCase(Locale.ENGLISH));
                        } else if (matchTokens.length > 2) {
                            replacer = matchTokens[2];
                        }

                        keywordCache.put(validKeyword, outputList);
                        break;
                    case TIME:
                        replacer = DateFormat.getTimeInstance(DateFormat.MEDIUM, ess.getI18n().getCurrentLocale()).format(new Date());
                        break;
                    case DATE:
                        replacer = DateFormat.getDateInstance(DateFormat.MEDIUM, ess.getI18n().getCurrentLocale()).format(new Date());
                        break;
                    case WORLDTIME12:
                        if (user != null) {
                            replacer = DescParseTickFormat.format12(user.getWorld() == null ? 0 : user.getWorld().getTime());
                        }
                        break;
                    case WORLDTIME24:
                        if (user != null) {
                            replacer = DescParseTickFormat.format24(user.getWorld() == null ? 0 : user.getWorld().getTime());
                        }
                        break;
                    case WORLDDATE:
                        if (user != null) {
                            replacer = DateFormat.getDateInstance(DateFormat.MEDIUM, ess.getI18n().getCurrentLocale()).format(DescParseTickFormat.ticksToDate(user.getWorld() == null ? 0 : user.getWorld().getFullTime()));
                        }
                        break;
                    case COORDS:
                        if (user != null) {
                            final Location location = user.getLocation();
                            replacer = tl("coordsKeyword", location.getBlockX(), location.getBlockY(), location.getBlockZ());
                        }
                        break;
                    case TPS:
                        replacer = NumberUtil.formatDouble(ess.getTimer().getAverageTPS());
                        break;
                    case UPTIME:
                        replacer = DateUtil.formatDateDiff(ManagementFactory.getRuntimeMXBean().getStartTime());
                        break;
                    case IP:
                        if (user != null) {
                            replacer = user.getBase().getAddress() == null || user.getBase().getAddress().getAddress() == null ? "" : user.getBase().getAddress().getAddress().toString();
                        }
                        break;
                    case ADDRESS:
                        if (user != null) {
                            replacer = user.getBase().getAddress() == null ? "" : user.getBase().getAddress().toString();
                        }
                        break;
                    case PLUGINS:
                        final StringBuilder pluginlistBuilder = new StringBuilder();
                        for (final Plugin p : ess.getServer().getPluginManager().getPlugins()) {
                            if (pluginlistBuilder.length() > 0) {
                                pluginlistBuilder.append(", ");
                            }
                            pluginlistBuilder.append(p.getDescription().getName());
                        }
                        replacer = pluginlistBuilder.toString();
                        break;
                    case VERSION:
                        replacer = ess.getServer().getVersion();
                        break;
                    default:
                        replacer = "N/A";
                        break;
                }

                if (this.replaceSpacesWithUnderscores) {
                    replacer = replacer.replaceAll("\\s", "_");
                }

                //If this is just a regular keyword, lets throw it into the cache
                if (validKeyword.getType().equals(KeywordCachable.CACHEABLE)) {
                    keywordCache.put(validKeyword, replacer);
                }
            }

            line = line.replace(fullMatch, replacer);
        } catch (final IllegalArgumentException ignored) {
        }

        return line;
    }

    @Override
    public List<String> getLines() {
        return replaced;
    }

    @Override
    public List<String> getChapters() {
        return input.getChapters();
    }

    @Override
    public Map<String, Integer> getBookmarks() {
        return input.getBookmarks();
    }
}
