/*
 * This file is licensed under the MIT License (MIT).
 *
 * Copyright (c) 2014 Daniel Ennis <http://aikar.co>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package co.aikar.timings;

import cn.nukkit.Server;
import cn.nukkit.command.CommandSender;
import cn.nukkit.command.ConsoleCommandSender;
import cn.nukkit.command.RemoteConsoleCommandSender;
import cn.nukkit.lang.TranslationContainer;
import cn.nukkit.nbt.stream.PGZIPOutputStream;
import cn.nukkit.timings.JsonUtil;
import cn.nukkit.utils.TextFormat;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.Deflater;


import static co.aikar.timings.TimingsManager.HISTORY;

public class TimingsExport extends Thread {
    private final CommandSender sender;
    private final JsonObject out;
    private final TimingsHistory[] history;

    private TimingsExport(CommandSender sender, JsonObject out, TimingsHistory[] history) {
        super("Timings paste thread");
        this.sender = sender;
        this.out = out;
        this.history = history;
    }

    /**
     * Builds a JSON timings report and sends it to Aikar's viewer
     *
     * @param sender Sender that issued the command
     */
    public static void reportTimings(CommandSender sender) {
        JsonObject out = new JsonObject();
        out.addProperty("version", Server.getInstance().getVersion());
        out.addProperty("maxplayers", Server.getInstance().getMaxPlayers());
        out.addProperty("start", TimingsManager.timingStart / 1000);
        out.addProperty("end", System.currentTimeMillis() / 1000);
        out.addProperty("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000);

        if (!Timings.isPrivacy()) {
            out.addProperty("server", Server.getInstance().getName());
            out.addProperty("motd", Server.getInstance().getMotd());
            out.addProperty("online-mode", false); //In MCPE we have permanent offline mode.
            out.addProperty("icon", ""); //"data:image/png;base64,"
        }

        final Runtime runtime = Runtime.getRuntime();
        RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();

        JsonObject system = new JsonObject();
        system.addProperty("timingcost", getCost());
        system.addProperty("name", System.getProperty("os.name"));
        system.addProperty("version", System.getProperty("os.version"));
        system.addProperty("jvmversion", System.getProperty("java.version"));
        system.addProperty("arch", System.getProperty("os.arch"));
        system.addProperty("maxmem", runtime.maxMemory());
        system.addProperty("cpu", runtime.availableProcessors());
        system.addProperty("runtime", ManagementFactory.getRuntimeMXBean().getUptime());
        system.addProperty("flags", String.join(" ", runtimeBean.getInputArguments()));
        system.add("gc", JsonUtil.mapToObject(ManagementFactory.getGarbageCollectorMXBeans(), (input) ->
                new JsonUtil.JSONPair(input.getName(), JsonUtil.toArray(input.getCollectionCount(), input.getCollectionTime()))));
        out.add("system", system);

        TimingsHistory[] history = HISTORY.toArray(new TimingsHistory[HISTORY.size() + 1]);
        history[HISTORY.size()] = new TimingsHistory(); //Current snapshot

        JsonObject timings = new JsonObject();
        for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) {
            for (Timing id : group.timings.stream().toArray(Timing[]::new)) {
                if (!id.timed && !id.isSpecial()) {
                    continue;
                }

                timings.add(String.valueOf(id.id), JsonUtil.toArray(group.id, id.name));
            }
        }

        JsonObject idmap = new JsonObject();
        idmap.add("groups", JsonUtil.mapToObject(TimingIdentifier.GROUP_MAP.values(), (group) ->
                new JsonUtil.JSONPair(group.id, group.name)));
        idmap.add("handlers", timings);
        idmap.add("worlds", JsonUtil.mapToObject(TimingsHistory.levelMap.entrySet(), (entry) ->
                new JsonUtil.JSONPair(entry.getValue(), entry.getKey())));
        idmap.add("tileentity", JsonUtil.mapToObject(TimingsHistory.blockEntityMap.entrySet(), (entry) ->
                new JsonUtil.JSONPair(entry.getKey(), entry.getValue())));
        idmap.add("entity", JsonUtil.mapToObject(TimingsHistory.entityMap.entrySet(), (entry) ->
                new JsonUtil.JSONPair(entry.getKey(), entry.getValue())));
        out.add("idmap", idmap);

        //Information about loaded plugins
        out.add("plugins", JsonUtil.mapToObject(Server.getInstance().getPluginManager().getPlugins().values(), (plugin) -> {
            JsonObject jsonPlugin = new JsonObject();
            jsonPlugin.addProperty("version", plugin.getDescription().getVersion());
            jsonPlugin.addProperty("description", plugin.getDescription().getDescription());// Sounds legit
            jsonPlugin.addProperty("website", plugin.getDescription().getWebsite());
            jsonPlugin.addProperty("authors", String.join(", ", plugin.getDescription().getAuthors()));
            return new JsonUtil.JSONPair(plugin.getName(), jsonPlugin);
        }));

        //Information on the users Config
        JsonObject config = new JsonObject();
        if (!Timings.getIgnoredConfigSections().contains("all")) {
            JsonObject nukkit = JsonUtil.toObject(Server.getInstance().getConfig().getRootSection());
            Timings.getIgnoredConfigSections().forEach(nukkit::remove);
            config.add("nukkit", nukkit);
        } else {
            config.add("nukkit", null);
        }
        out.add("config", config);

        new TimingsExport(sender, out, history).start();
    }

    private static long getCost() {
        int passes = 200;
        Timing SAMPLER1 = TimingsManager.getTiming(null, "Timings sampler 1", null);
        Timing SAMPLER2 = TimingsManager.getTiming(null, "Timings sampler 2", null);
        Timing SAMPLER3 = TimingsManager.getTiming(null, "Timings sampler 3", null);
        Timing SAMPLER4 = TimingsManager.getTiming(null, "Timings sampler 4", null);
        Timing SAMPLER5 = TimingsManager.getTiming(null, "Timings sampler 5", null);
        Timing SAMPLER6 = TimingsManager.getTiming(null, "Timings sampler 6", null);

        long start = System.nanoTime();
        for (int i = 0; i < passes; i++) {
            SAMPLER1.startTiming();
            SAMPLER2.startTiming();
            SAMPLER3.startTiming();
            SAMPLER4.startTiming();
            SAMPLER5.startTiming();
            SAMPLER6.startTiming();
            SAMPLER6.stopTiming();
            SAMPLER5.stopTiming();
            SAMPLER4.stopTiming();
            SAMPLER3.stopTiming();
            SAMPLER2.stopTiming();
            SAMPLER1.stopTiming();
        }

        long timingsCost = (System.nanoTime() - start) / passes / 6;

        SAMPLER1.reset(true);
        SAMPLER2.reset(true);
        SAMPLER3.reset(true);
        SAMPLER4.reset(true);
        SAMPLER5.reset(true);
        SAMPLER6.reset(true);

        return timingsCost;
    }

    @Override
    public synchronized void start() {
        if (this.sender instanceof RemoteConsoleCommandSender) {
            this.sender.sendMessage(new TranslationContainer("nukkit.command.timings.rcon"));
            run();
        } else {
            super.start();
        }
    }

    @Override
    public void run() {
        this.sender.sendMessage(new TranslationContainer("nukkit.command.timings.uploadStart"));
        this.out.add("data", JsonUtil.mapToArray(this.history, TimingsHistory::export));

        String response = null;
        try {
            HttpURLConnection con = (HttpURLConnection) new URL("http://timings.aikar.co/post").openConnection();
            con.setDoOutput(true);
            con.setRequestProperty("User-Agent", "Nukkit/" + Server.getInstance().getName() + "/" + InetAddress.getLocalHost().getHostName());
            con.setRequestMethod("POST");
            con.setInstanceFollowRedirects(false);

            PGZIPOutputStream request = new PGZIPOutputStream(con.getOutputStream());
            request.setLevel(Deflater.BEST_COMPRESSION);

            request.write(new Gson().toJson(this.out).getBytes("UTF-8"));
            request.close();

            response = getResponse(con);

            if (con.getResponseCode() != 302) {
                this.sender.sendMessage(new TranslationContainer("nukkit.command.timings.uploadError", new String[]{String.valueOf(con.getResponseCode()), con.getResponseMessage()}));
                if (response != null) {
                    Server.getInstance().getLogger().alert(response);
                }
                return;
            }

            String location = con.getHeaderField("Location");
            this.sender.sendMessage(new TranslationContainer("nukkit.command.timings.timingsLocation", location));
            if (!(this.sender instanceof ConsoleCommandSender)) {
                Server.getInstance().getLogger().info(Server.getInstance().getLanguage().translateString("nukkit.command.timings.timingsLocation", location));
            }

            if (response != null && !response.isEmpty()) {
                Server.getInstance().getLogger().info(Server.getInstance().getLanguage().translateString("nukkit.command.timings.timingsResponse", response));
            }

            File timingFolder = new File(Server.getInstance().getDataPath() + File.separator + "timings");
            timingFolder.mkdirs();
            String fileName = timingFolder + File.separator + new SimpleDateFormat("'timings-'yyyy-MM-dd-hh-mm'.txt'").format(new Date());

            FileWriter writer = new FileWriter(fileName);
            writer.write(Server.getInstance().getLanguage().translateString("nukkit.command.timings.timingsLocation", location) + "\n\n");
            writer.write(new GsonBuilder().setPrettyPrinting().create().toJson(this.out));
            writer.close();

            Server.getInstance().getLogger().info(Server.getInstance().getLanguage().translateString("nukkit.command.timings.timingsWrite", fileName));
        } catch (IOException exception) {
            this.sender.sendMessage(TextFormat.RED + "" + new TranslationContainer("nukkit.command.timings.reportError"));
            if (response != null) {
                Server.getInstance().getLogger().alert(response);
            }
            Server.getInstance().getLogger().logException(exception);
        }
    }

    private String getResponse(HttpURLConnection con) throws IOException {
        InputStream is = null;
        try {
            is = con.getInputStream();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            byte[] b = new byte[1024];
            int bytesRead;
            while ((bytesRead = is.read(b)) != -1) {
                bos.write(b, 0, bytesRead);
            }
            return bos.toString();

        } catch (IOException exception) {
            this.sender.sendMessage(TextFormat.RED + "" + new TranslationContainer("nukkit.command.timings.reportError"));
            Server.getInstance().getLogger().warning(con.getResponseMessage(), exception);
            return null;
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }
}
