package net.ME1312.SubServers.Client.Bukkit.Network;

import net.ME1312.SubServers.Client.Bukkit.Library.Exception.IllegalPacketException;
import net.ME1312.SubServers.Client.Bukkit.Library.Version.Version;
import net.ME1312.SubServers.Client.Bukkit.Network.Packet.*;
import net.ME1312.SubServers.Client.Bukkit.SubPlugin;
import org.bukkit.Bukkit;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;

public final class SubDataClient {
    private static HashMap<Class<? extends PacketOut>, String> pOut = new HashMap<Class<? extends PacketOut>, String>();
    private static HashMap<String, PacketIn> pIn = new HashMap<String, PacketIn>();
    private static boolean defaults = false;
    private PrintWriter writer;
    private Socket socket;
    private String name;
    private SubPlugin plugin;

    /**
     * SubServers Client Instance
     *
     * @param plugin SubPlugin
     * @param address Bind Address
     * @param port Port
     * @throws IOException
     */
    public SubDataClient(SubPlugin plugin, String name, InetAddress address, int port) throws IOException {
        socket = new Socket(address, port);
        this.plugin = plugin;
        this.name = name;
        this.writer = new PrintWriter(socket.getOutputStream(), true);

        if (!defaults) loadDefaults();
        loop();

        Bukkit.getScheduler().runTaskLater(plugin, () -> sendPacket(new PacketAuthorization(plugin)), 10);
    }

    private void loadDefaults() {
        defaults = true;

        registerPacket(new PacketAuthorization(plugin), "Authorization");
        registerPacket(new PacketCommandServer(), "SubCommandServer");
        registerPacket(new PacketCreateServer(), "SubCreateServer");
        registerPacket(new PacketDownloadHostInfo(), "SubDownloadHostInfo");
        registerPacket(new PacketDownloadLang(plugin), "SubDownloadLang");
        registerPacket(new PacketDownloadPlayerList(), "SubDownloadPlayerList");
        registerPacket(new PacketDownloadServerInfo(), "SubDownloadServerInfo");
        registerPacket(new PacketDownloadServerList(), "SubDownloadServerList");
        registerPacket(new PacketInfoPassthrough(), "SubInfoPassthrough");
        registerPacket(new PacketInRunEvent(), "SubRunEvent");
        registerPacket(new PacketInShutdown(), "SubShutdown");
        registerPacket(new PacketLinkServer(plugin), "SubLinkServer");
        registerPacket(new PacketStartServer(), "SubStartServer");
        registerPacket(new PacketStopServer(), "SubStopServer");
        registerPacket(new PacketTeleportPlayer(), "SubTeleportPlayer");

        registerPacket(PacketAuthorization.class, "Authorization");
        registerPacket(PacketCommandServer.class, "SubCommandServer");
        registerPacket(PacketCreateServer.class, "SubCreateServer");
        registerPacket(PacketDownloadHostInfo.class, "SubDownloadHostInfo");
        registerPacket(PacketDownloadLang.class, "SubDownloadLang");
        registerPacket(PacketDownloadPlayerList.class, "SubDownloadPlayerList");
        registerPacket(PacketDownloadServerInfo.class, "SubDownloadServerInfo");
        registerPacket(PacketDownloadServerList.class, "SubDownloadServerList");
        registerPacket(PacketInfoPassthrough.class, "SubInfoPassthrough");
        registerPacket(PacketLinkServer.class, "SubLinkServer");
        registerPacket(PacketStartServer.class, "SubStartServer");
        registerPacket(PacketStopServer.class, "SubStopServer");
        registerPacket(PacketTeleportPlayer.class, "SubTeleportPlayer");
    }

    private void loop() {
        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String input;
                while ((input = in.readLine()) != null) {
                    try {
                        JSONObject json = new JSONObject(input);
                        PacketIn packet = decodePacket(json);
                        try {
                            packet.execute((json.keySet().contains("c")) ? json.getJSONObject("c") : null);
                        } catch (Exception e) {
                            new InvocationTargetException(e, "Exception while executing PacketIn").printStackTrace();
                        }
                    } catch (IllegalPacketException e) {
                        e.printStackTrace();
                    } catch (JSONException e) {
                        new IllegalPacketException("Unknown Packet Format: " + input).printStackTrace();
                    }
                }
                try {
                    destroy(true);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            } catch (Exception e) {
                if (!(e instanceof SocketException)) e.printStackTrace();
                try {
                    destroy(true);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        });
    }

    /**
     * Gets the Assigned Server Name
     *
     * @return Server Name
     */
    public String getName() {
        return name;
    }

    /**
     * Gets the Server Socket
     *
     * @return Server Socket
     */
    public Socket getClient() {
        return socket;
    }

    /**
     * Register Packet to the Network
     *
     * @param packet PacketIn to register
     * @param handle Handle to Bind
     */
    public static void registerPacket(PacketIn packet, String handle) {
        pIn.put(handle, packet);
    }

    /**
     * Register Packet to the Network
     *
     * @param packet PacketOut to register
     * @param handle Handle to bind
     */
    public static void registerPacket(Class<? extends PacketOut> packet, String handle) {
        pOut.put(packet, handle);
    }

    /**
     * Grab PacketIn Instance via handle
     *
     * @param handle Handle
     * @return PacketIn
     */
    public static PacketIn getPacket(String handle) {
        return pIn.get(handle);
    }

    /**
     * Send Packet to Client
     *
     * @param packet Packet to send
     */
    public void sendPacket(PacketOut packet) {
        try {
            writer.println(encodePacket(packet));
        } catch (IllegalPacketException e) {
            e.printStackTrace();
        }
    }

    /**
     * JSON Encode PacketOut
     *
     * @param packet PacketOut
     * @return JSON Formatted Packet
     * @throws IllegalPacketException
     */
    protected static JSONObject encodePacket(PacketOut packet) throws IllegalPacketException {
        JSONObject json = new JSONObject();

        if (!pOut.keySet().contains(packet.getClass())) throw new IllegalPacketException("Unknown PacketOut Channel: " + packet.getClass().getCanonicalName());
        if (packet.getVersion().toString() == null) throw new NullPointerException("PacketOut Version cannot be null: " + packet.getClass().getCanonicalName());

        JSONObject contents = packet.generate();
        json.put("h", pOut.get(packet.getClass()));
        json.put("v", packet.getVersion().toString());
        if (contents != null) json.put("c", contents);
        return json;
    }

    /**
     * JSON Decode PacketIn
     *
     * @param json JSON to Decode
     * @return PacketIn
     * @throws IllegalPacketException
     * @throws InvocationTargetException
     */
    protected static PacketIn decodePacket(JSONObject json) throws IllegalPacketException, InvocationTargetException {
        if (!json.keySet().contains("h") || !json.keySet().contains("v")) throw new IllegalPacketException("Unknown Packet Format: " + json.toString());
        if (!pIn.keySet().contains(json.getString("h"))) throw new IllegalPacketException("Unknown PacketIn Channel: " + json.getString("h"));

        PacketIn packet = pIn.get(json.getString("h"));
        if (!new Version(json.getString("v")).equals(packet.getVersion())) throw new IllegalPacketException("Packet Version Mismatch in " + json.getString("h") + ": " + json.getString("v") + "->" + packet.getVersion().toString());
        return packet;
    }

    /**
     * Drops All Connections and Stops the SubData Listener
     *
     * @throws IOException
     */
    public void destroy(boolean reconnect) throws IOException {
        if (socket != null) {
            final Socket socket = this.socket;
            this.socket = null;
            if (!socket.isClosed()) socket.close();
            Bukkit.getLogger().info("SubServers > The SubData Connection was closed");
            if (reconnect) {
                Bukkit.getLogger().info("SubServers > Attempting to reconnect in 10 seconds");
                Bukkit.getScheduler().runTaskLater(plugin, () -> {
                    try {
                        plugin.subdata = new SubDataClient(plugin, name, socket.getInetAddress(), socket.getPort());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }, 10 * 20);
            }
            plugin.subdata = null;
        }
    }
}
