package com.github.games647.lagmonitor.command.timing;

import co.aikar.timings.TimingHistory;
import co.aikar.timings.Timings;
import co.aikar.timings.TimingsManager;

import com.github.games647.lagmonitor.LagMonitor;
import com.github.games647.lagmonitor.Pages;
import com.github.games647.lagmonitor.traffic.Reflection;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.Maps;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.TextComponent;

import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;

import static com.github.games647.lagmonitor.util.LagUtils.round;

/**
 * Paper and Sponge uses a new timings system (v2).
 * Missing data:
 * * TicksRecord
 * -> player ticks
 * -> timedTicks
 * -> entityTicks
 * -> activatedEntityTicks
 * -> tileEntityTicks
 * * MinuteReport
 * -> time
 * -> tps
 * -> avgPing
 * -> fullServerTick
 * -> ticks
 * * World data
 * -> worldName
 * -> tileEntities
 * -> entities
 *
 * => This concludes to the fact that the big benefits from Timings v2 isn't available. For example you cannot
 * scroll through your history
 */
public class PaperTimingsCommand extends TimingCommand {

    //TODO: Change to MethodHandles
    private static final String TIMINGS_PACKAGE = "co.aikar.timings";

    private static final String EXPORT_CLASS = TIMINGS_PACKAGE + '.' + "TimingsExport";
    private static final String HANDLER_CLASS = TIMINGS_PACKAGE + '.' + "TimingHandler";
    private static final String HISTORY_ENTRY_CLASS = TIMINGS_PACKAGE + '.' + "TimingHistoryEntry";
    private static final String DATA_CLASS = TIMINGS_PACKAGE + '.' + "TimingData";

    private static final ChatColor HEADER_COLOR = ChatColor.YELLOW;

    private int historyInterval;

    public PaperTimingsCommand(LagMonitor plugin) {
        super(plugin);

        try {
            historyInterval = Reflection.getField("com.destroystokyo.paper.PaperConfig", "config"
            , YamlConfiguration.class).get(null).getInt("timings.history-interval");
        } catch (IllegalArgumentException illegalArgumentException) {
            //cannot find paper spigot
            historyInterval = -1;
        }
    }

    @Override
    protected boolean isTimingsEnabled() {
        return Timings.isTimingsEnabled();
    }

    @Override
    protected void sendTimings(CommandSender sender) {
        EvictingQueue<TimingHistory> history = Reflection.getField(TimingsManager.class, "HISTORY", EvictingQueue.class)
                .get(null);

        TimingHistory lastHistory = history.peek();
        if (lastHistory == null) {
            sendError(sender, "Not enough data collected yet");
            return;
        }

        List<BaseComponent[]> lines = new ArrayList<>();
        printTimings(lines, lastHistory);

        Pages pagination = new Pages("Paper Timings", lines);
        pagination.send(sender);

        plugin.getPageManager().setPagination(sender.getName(), pagination);
    }

    public void printTimings(Collection<BaseComponent[]> lines, TimingHistory lastHistory) {
        printHeadData(lastHistory, lines);

        Map<Integer, String> idHandler = Maps.newHashMap();

        Map<?, ?> groups = Reflection.getField(TIMINGS_PACKAGE + ".TimingIdentifier", "GROUP_MAP", Map.class).get(null);
        for (Object group : groups.values()) {
            String groupName = Reflection.getField(group.getClass(), "name", String.class).get(group);
            Iterable<?> handlers = Reflection.getField(group.getClass(), "handlers", ArrayDeque.class).get(group);
            for (Object handler : handlers) {
                int id = Reflection.getField(HANDLER_CLASS, "id", Integer.TYPE).get(handler);
                String name = Reflection.getField(HANDLER_CLASS, "name", String.class).get(handler);
                if (name.contains("Combined")) {
                    idHandler.put(id, "Combined " + groupName);
                } else {
                    idHandler.put(id, name);
                }
            }
        }

        //TimingHistoryEntry
        Object[] entries = Reflection.getField(TimingHistory.class, "entries", Object[].class).get(lastHistory);
        for (Object entry : entries) {
            Object parentData = Reflection.getField(HISTORY_ENTRY_CLASS, "data", Object.class).get(entry);
            int childId = Reflection.getField(DATA_CLASS, "id", Integer.TYPE).get(parentData);

            String handlerName = idHandler.get(childId);
            String parentName;
            if (handlerName == null) {
                parentName = "Unknown-" + childId;
            } else {
                parentName = handlerName;
            }

            int parentCount = Reflection.getField(DATA_CLASS, "count", Integer.TYPE).get(parentData);
            long parentTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(parentData);

//            long parentLagCount = Reflection.getField(DATA_CLASS, "lagCount", Integer.TYPE).get(parentData);
//            long parentLagTime = Reflection.getField(DATA_CLASS, "lagTime", Long.TYPE).get(parentData);
            lines.add(new ComponentBuilder(parentName).color(HEADER_COLOR)
                    .append(" Count: " + parentCount + " Time: " + parentTime).create());

            Object[] children = Reflection.getField(HISTORY_ENTRY_CLASS, "children", Object[].class).get(entry);
            for (Object childData : children) {
                printChildren(parentData, childData, idHandler, lines);
            }
        }
    }

    private void printChildren(Object parent, Object childData, Map<Integer, String> idMap,
                               Collection<BaseComponent[]> lines) {
        int childId = Reflection.getField(DATA_CLASS, "id", Integer.TYPE).get(childData);

        String handlerName = idMap.get(childId);
        String childName;
        if (handlerName == null) {
            childName = "Unknown-" + childId;
        } else {
            childName = handlerName;
        }

        int childCount = Reflection.getField(DATA_CLASS, "count", Integer.TYPE).get(childData);
        long childTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(childData);

        long parentTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(parent);
        double percent = (double) childTime / parentTime;

        lines.add(new ComponentBuilder("    " + childName + " Count: " + childCount + " Time: " + childTime
                + ' ' + round(percent) + '%')
                .color(PRIMARY_COLOR.asBungee()).create());
    }

    private void printHeadData(TimingHistory lastHistory, Collection<BaseComponent[]> lines) {
        // Represents all time spent running the server this history
        long totalTime = Reflection.getField(TimingHistory.class, "totalTime", Long.TYPE).get(lastHistory);
        long totalTicks = Reflection.getField(TimingHistory.class, "totalTicks", Long.TYPE).get(lastHistory);

        long cost = (long) Reflection.getMethod(EXPORT_CLASS, "getCost").invoke(null);
        lines.add(new ComponentBuilder("Cost: ")
                .color(PRIMARY_COLOR.asBungee())
                .append(Long.toString(cost)).color(SECONDARY_COLOR.asBungee()).create());

        double totalSeconds = (double) totalTime / 1000 / 1000;

        long playerTicks = TimingHistory.playerTicks;
        long tileEntityTicks = TimingHistory.tileEntityTicks;
        long activatedEntityTicks = TimingHistory.activatedEntityTicks;
        long entityTicks = TimingHistory.entityTicks;

        double activatedAvgEntities = (double) activatedEntityTicks / totalTicks;
        double totalAvgEntities = (double) entityTicks / totalTicks;

        double averagePlayers = (double) playerTicks / totalTicks;

        double desiredTicks = 20 * historyInterval;
        double averageTicks = totalTicks / desiredTicks * 20;

        String format = ChatColor.DARK_AQUA + "%s" + ' ' + ChatColor.GRAY + "%s";

        //head data
        lines.add(TextComponent.fromLegacyText(String.format(format, "Total (sec):", round(totalSeconds))));
        lines.add(TextComponent.fromLegacyText(String.format(format, "Ticks:", round(totalTicks))));
        lines.add(TextComponent.fromLegacyText(String.format(format, "Avg ticks:", round(averageTicks))));
//        lines.add(TextComponent.fromLegacyText(String.format(format, "Server Load:", round(serverLoad))));
        lines.add(TextComponent.fromLegacyText(String.format(format, "AVG Players:", round(averagePlayers))));

        lines.add(TextComponent.fromLegacyText(String.format(format, "Activated Entities:", round(activatedAvgEntities))
                + " / " + round(totalAvgEntities)));
    }
}