package net.knarcraft.serverlauncher.profile;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.knarcraft.serverlauncher.server.AdvancedServerType;
import net.knarcraft.serverlauncher.server.Server;
import net.knarcraft.serverlauncher.server.ServerType;
import net.knarcraft.serverlauncher.userinterface.GUI;
import net.knarcraft.serverlauncher.userinterface.ServerConsoles;
import net.knarcraft.serverlauncher.userinterface.ServerTab;
import net.knarcraft.serverlauncher.Main;

import javax.swing.*;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.Executors;

import static net.knarcraft.serverlauncher.Shared.downloadFile;
import static net.knarcraft.serverlauncher.Shared.readFile;
import static net.knarcraft.serverlauncher.Shared.stringBetween;

/**
 * Contains all user settings, and a list of servers.
 *
 * @author      Kristian Knarvik <kristian.knarvik@knett.no>
 * @version     1.0.0
 * @since       1.0.0
 */
public class Profile {
    private static final ArrayList<Profile> profiles = new ArrayList<>();
    private static Profile current;
    private static GUI gui;
    private static final String profilesDir = Main.getAppDir() + File.separator +  "files";
    private static final String profilesFile = Main.getAppDir() + File.separator +  "files" + File.separator + "Profiles.txt";
    private static final String jarDir = Main.getAppDir() + File.separator + "files" + File.separator + "Jars" + File.separator;

    private final ArrayList<Collection> collections;
    private final String name;
    private boolean runInBackground;
    private int delayStartup;
    private boolean downloadJars;
    private static String vanillaVersion;
    private static String snapshotVersion;
    private static String bungeeVersion;

    private Profile(String name) {
        this.collections = new ArrayList<>();
        this.name = name;
        this.runInBackground = false;
        this.delayStartup = 0;
        this.downloadJars = false;
        profiles.add(this);
        if (current == null) {
            current = this;
        }
    }

    private Profile(String name, boolean runInBackground, int delayStartup, boolean downloadJars) {
        this.collections = new ArrayList<>();
        this.name = name;
        this.runInBackground = runInBackground;
        this.delayStartup = delayStartup;
        this.downloadJars = downloadJars;
        profiles.add(this);
        if (current == null) {
            current = this;
        }
    }

    public static GUI getGUI() {
        return gui;
    }

    public boolean getRunInBackground() {
        return this.runInBackground;
    }

    public int getDelayStartup() {
        return this.delayStartup;
    }

    public boolean getDownloadJars() {
        return this.downloadJars;
    }

    public static Profile getCurrent() {
        return current;
    }

    public ArrayList<Collection> getCollections() {
        return this.collections;
    }

    public String getName() {
        return this.name;
    }

    /**
     * Gets a Collection object by name.
     *
     * @param name  The name of the collection.
     * @return      A collection object.
     */
    public Collection getCollection(String name) {
        for (Collection collection : this.collections) {
            if (collection.getName().equals(name)) {
                return collection;
            }
        }
        return null;
    }

    public static Profile getProfile(String name) {
        for (Profile profile : profiles) {
            if (profile.name.equals(name)) {
                return profile;
            }
        }
        return null;
    }

    public static ArrayList<Profile> getProfiles() {
        return profiles;
    }

    public void setRunInBackground(boolean value) {
        this.runInBackground = value;
    }

    public void setDelayStartup(int value) {
        if (value >= 0) {
            this.delayStartup = value;
        }
    }

    public void setDownloadJars(boolean value) {
        this.downloadJars = value;
    }

    /**
     * Set the current profile to the profile with a certain name.
     *
     * @param name  The name of the profile
     */
    public static  void setCurrent(String name) {
        for (Profile profile : profiles) {
            if (profile.name.equals(name)) {
                current = profile;
                break;
            }
        }
    }

    /**
     * Adds a collection to the profile if the name is valid.
     *
     * @param name  The name of the collection and its elements.
     */
    public void addCollection(String name) {
        if (name == null) { //If a user cancels or crosses out window
            return;
        }
        if (getCollection(name) == null &&
                !name.equals("") &&
                !name.equals("All") &&
                !name.contains("!") &&
                !name.contains("?") &&
                !name.contains(";")
                ) {
            collections.add(new Collection(name));
        } else {
            JOptionPane.showMessageDialog(
                    null,
                    "A server name must my unique and not empty or \"All\"." +
                            "It can't contain any of the characters \"!\", \"?\" or \";\".",
                    "Error",
                    JOptionPane.ERROR_MESSAGE
            );
        }
    }

    /**
     * Adds a profile if the name is valid and unique.
     *
     * @param name  The name of the new profile.
     */
    public static void addProfile(String name) {
        if (name == null) { //If a user cancels or crosses out window
            return;
        }
        if (name.equals("") && !name.contains("!") && !name.contains("?") && !name.contains(";")) {
            JOptionPane.showMessageDialog(
                    null,
                    "Profile name can't be blank.",
                    "Error",
                    JOptionPane.ERROR_MESSAGE
            );
            return;
        }
        for (Profile profile : profiles) {
            if (profile.name.equals(name)) {
                JOptionPane.showMessageDialog(
                        null,
                        "There is already a profile with this name.",
                        "Error",
                        JOptionPane.ERROR_MESSAGE
                );
                return;
            }
        }
        new Profile(name);
    }

    public void removeCollection(String name) {
        for (int i = 0; i < collections.size(); i++) {
            if (collections.get(i).getName().equals(name)) {
                this.collections.remove(i);
                gui.update();
                break;
            }
        }
    }

    public static void removeProfile(String name) {
        if (profiles.size() > 1) {
            for (int i = 0; i < profiles.size(); i++) {
                if (profiles.get(i).name.equals((name))) {
                    profiles.remove(i);
                }
            }
        }
    }

    public void updateConsoles() {
        JTabbedPane consolesTab = ServerConsoles.getTab();
        consolesTab.removeAll();
        for (Collection collection : collections) {
            consolesTab.add(collection.getName(), collection.getServerConsole().getPanel());
        }
    }

    /**
     * Sends a command to a server, or all servers
     *
     * @param serverName    The target server
     * @param command       The command to send.
     */
    public void sendCommand(String serverName, String command) {
        if (serverName.equals("All")) {
            for (Collection collection : this.collections) {
                try {
                    collection.getServer().sendCommand(command);
                } catch (IOException e) {
                    JOptionPane.showMessageDialog(
                            null,
                            "Server " + collection.getName() + " caused an exception.",
                            "Error",
                            JOptionPane.ERROR_MESSAGE
                    );
                }
            }
        } else {
            Collection collection = getCollection(serverName);
            if (collection != null) {
                Server target = collection.getServer();
                try {
                    target.sendCommand(command);
                } catch (IOException e) {
                    JOptionPane.showMessageDialog(
                            null,
                            "Server " + target.getName() + " caused an exception.",
                            "Error",
                            JOptionPane.ERROR_MESSAGE
                    );
                }
            } else {
                JOptionPane.showMessageDialog(
                        null,
                        "Server " + serverName + " is invalid.",
                        "Error", JOptionPane.ERROR_MESSAGE
                );
            }
        }
    }

    /**
     * Reads all server tabs, and saves it to the variables of the corresponding servers.
     * Saves all profiles and servers to a text file.
     */
    public void save() throws FileNotFoundException {
        for (Collection collection : this.collections) {
            Server server = collection.getServer();
            ServerTab serverTab = collection.getServerTab();
            server.setPath(serverTab.getPath());
            server.setMaxRam(serverTab.getMaxRam());
            server.setType(ServerType.getByName(serverTab.getType()));
            try {
                server.setServerVersion(serverTab.getVersion());
            } catch (IllegalArgumentException e) {
                JOptionPane.showMessageDialog(
                        null,
                        "Invalid server version for " + server.getName(),
                        "Error",
                        JOptionPane.ERROR_MESSAGE
                );
            }
            server.toggle(serverTab.enabled());
        }
        if (!new File(profilesDir).exists() && !new File(profilesDir).mkdirs()) {
            JOptionPane.showMessageDialog(
                    null,
                    "Unable to create the folder " + profilesDir,
                    "Error",
                    JOptionPane.ERROR_MESSAGE
            );
            throw new FileNotFoundException("Unable to create the profiles folder: " + profilesDir);
        }
        try (PrintWriter file = new PrintWriter(profilesFile)) {
            file.println(String.format(
                    "%s;%s;%s;%s;%d;%d",
                    current.name,
                    vanillaVersion,
                    snapshotVersion,
                    bungeeVersion,
                    gui.getSize().width,
                    gui.getSize().height
            ));
            file.close();
            for (Profile profile : profiles) {
                StringBuilder saveString = new StringBuilder(String.format(
                        "%s;%b;%d;%b?",
                        profile.name,
                        profile.runInBackground,
                        profile.delayStartup,
                        profile.downloadJars)
                );
                for (Collection collection : profile.collections) {
                    Server server = collection.getServer();
                    saveString.append(String.format(
                            "%s;%s;%b;%s;%s;%s;%s;%s;%s;%s!",
                            server.getName(),
                            server.getPath(),
                            server.isEnabled(),
                            server.getTypeName(),
                            server.getServerVersion(),
                            server.getMaxRam(),
                            server.getVanillaVersion(),
                            server.getSnapshotVersion(),
                            server.getSpongeVanillaVersion(),
                            server.getBungeeVersion()
                            )
                    );
                }
                saveString = new StringBuilder(saveString.substring(0, saveString.length() - 1));
                try (PrintWriter fileAppend = new PrintWriter(new FileWriter(
                        profilesFile,
                        true
                ))) {
                    fileAppend.println(saveString);
                } catch (IOException e) {
                    if (gui != null) {
                        JOptionPane.showMessageDialog(
                                null,
                                "Unable to save to file. Try running the software as an administrator.",
                                "Error",
                                JOptionPane.ERROR_MESSAGE
                        );
                    } else {
                        System.out.println("Unable to save to file. Try running the software as an administrator.");
                    }
                    throw new FileNotFoundException("Unable to save to the profiles file.");
                }
            }
        } catch (IOException e) {
            if (gui != null) {
                JOptionPane.showMessageDialog(
                        null,
                        "Unable to save to file. Try running the software as an administrator.",
                        "Error",
                        JOptionPane.ERROR_MESSAGE
                );
            }
            throw new FileNotFoundException("Unable to create the profiles file");
        }
    }

    /**
     * Reads profiles and servers from a text file.
     */
    public static void load() {
        try (Scanner in = new Scanner(new File(profilesFile))) {
            try {
                String[] staticData = in.nextLine().split(";", -1);
                String profileName = staticData[0];
                vanillaVersion = staticData[1];
                snapshotVersion = staticData[2];
                bungeeVersion = staticData[3];
                int guiWidth = Integer.parseInt(staticData[4]);
                int guiHeight = Integer.parseInt(staticData[5]);
                gui = new GUI(guiWidth, guiHeight);
                while (in.hasNextLine()) {
                    String line = in.nextLine();
                    if (line.contains("?")) {
                        String[] data = line.split("\\?");
                        String[] profileData = data[0].split(";", -1);
                        Profile profile = parseProfile(profileData);
                        if (data[1].contains("!")) {
                            String[] servers = data[1].split("!", -1);
                            for (String server : servers) {
                                String[] serverData = server.split(";", -1);
                                parseServer(profile, serverData);
                            }
                        } else {
                            String[] serverData = data[1].split(";", -1);
                            parseServer(profile, serverData);
                        }
                    } else {
                        String[] profileData = line.split(";", -1);
                        parseProfile(profileData);
                    }
                }
                current = getProfile(profileName);
            } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(
                        null,
                        "Invalid Profile.txt file. Profiles could not be loaded. If this error persists, please manually delete the file.",
                        "Error",
                        JOptionPane.ERROR_MESSAGE
                );
                System.exit(1);
            }
            if (profiles.size() == 0) {
                addProfile("Default");
            }
        } catch (FileNotFoundException e) {
            JOptionPane.showMessageDialog(
                    null,
                    "A profiles file was not found. Default profile was created.",
                    "Info",
                    JOptionPane.INFORMATION_MESSAGE
            );
            gui = new GUI();
            addProfile("Default");
        }
        gui.update();
        gui.updateProfiles();
        current.updateConsoles();
        if (current.downloadJars) {
            Executors.newSingleThreadExecutor().execute(() -> {
                try {
                    downloadJars();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
        if (current.runInBackground) {
            Executors.newSingleThreadExecutor().execute(Server::startServers);
            gui.hide();
        }
    }

    /**
     * Parses a profile, and creates a profile with the data.
     *
     * @param profileData   The data of the new profile
     * @return              The new profile
     */
    private static Profile parseProfile(String[] profileData) {
        return new Profile(
                profileData[0],
                Boolean.parseBoolean(profileData[1]),
                Integer.parseInt(profileData[2]),
                Boolean.parseBoolean(profileData[3])
        );
    }

    /**
     * Parses a server, and creates a new collection.
     *
     * @param profile       The profile which to add the collection
     * @param serverData    The data to parse
     */
    private static void parseServer(Profile profile, String[] serverData) {
        profile.collections.add(new Collection(
                serverData[0],
                serverData[1],
                Boolean.parseBoolean(serverData[2]),
                serverData[3],
                serverData[4],
                serverData[5],
                serverData[6],
                serverData[7],
                serverData[8],
                serverData[9])
        );
    }

    /**
     * Downloads all jars to the program directory.
     *
     * @throws IOException  On version file failure or folder creation failure
     */
    public static void downloadJars() throws IOException {
        if (!new File(jarDir).exists() && !new File(jarDir).mkdirs()) {
            JOptionPane.showMessageDialog(
                    null,
                    "Could not create the Jars folder. Please run the program with admin permissions, or move it to a writable directory.",
                    "Error",
                    JOptionPane.ERROR_MESSAGE
            );
            throw new FileNotFoundException("Unable to create jars folder");
        }
        try {
            downloadAll();
            printToGui("Finished downloading jars");
        } catch (FileNotFoundException e) {
            printToGui("One or more downloads failed: " + e.getMessage());
        }
    }

    private static void printToGui(String str) {
        if (gui != null) {
            gui.setStatus(str);
        } else {
            System.out.println(str);
        }
    }

    /**
     * Downloads jar files for all possible server versions.
     *
     * @throws IOException If a jar fails to download.
     */
    private static void downloadAll() throws IOException {
        for (ServerType type : ServerType.getServerTypes()) {
            String url = Objects.requireNonNull(type).getDownloadURL(), name = type.getName(), newestVersion;
            AdvancedServerType advType = type instanceof AdvancedServerType ? (AdvancedServerType) type : null;
            for (String version : type.getVersions()) {
                Boolean success;
                printToGui("Downloading: " + name + version + ".jar");
                File file = new File(jarDir + type.getName() + version + ".jar");
                Path filePath = Paths.get(jarDir + type.getName() + version + ".jar");
                switch (type.getName()) {
                    case "Vanilla":
                    case "Snapshot":
                        if (version.equals("Latest")) {
                            String versionText = readFile(Objects.requireNonNull(advType).getVersionURL());
                            JsonObject jsonObject = new JsonParser().parse(versionText).getAsJsonObject();
                            String latest = jsonObject.getAsJsonObject("latest").get("release").getAsString();
                            JsonElement ver = jsonObject.getAsJsonArray("versions").get(0);
                            String versionFile = ver.getAsJsonObject().get("url").getAsString();

                            versionText = readFile(versionFile);
                            jsonObject = new JsonParser().parse(versionText).getAsJsonObject();
                            String jarFile = jsonObject.getAsJsonObject("downloads").getAsJsonObject("server").get("url").getAsString();

                            setVersion(name, latest);
                            success = (file.isFile() && latest.equals(getVersion(name))) || downloadFile(jarFile, filePath);
                        } else {
                            success = file.isFile() || downloadFile(url + version + Objects.requireNonNull(advType).getDownloadURLPart() + version + ".jar", filePath);
                        }
                        break;
                    case "Spigot":
                    case "Craftbukkit":
                    case "MCPCplus":
                        success = file.isFile() || downloadFile(url + name + version + ".jar", filePath);
                        break;
                    case "SpongeVanilla":
                        newestVersion = stringBetween(readFile(Objects.requireNonNull(advType).getVersionURL() + version), advType.getSrcStart(), advType.getSrcEnd());
                        success = file.isFile() || downloadFile(url + newestVersion + advType.getDownloadURLPart() + newestVersion + ".jar", filePath);
                        break;
                    case "Bungee":
                        newestVersion = stringBetween(readFile(Objects.requireNonNull(advType).getVersionURL()), advType.getSrcStart(), advType.getSrcEnd());
                        setVersion(name, newestVersion);
                        success = (file.isFile() && newestVersion.equals(getVersion(name))) || downloadFile(url, filePath);
                        break;
                    default:
                        success = true;
                }
                if (!success) {
                    printToGui("Error downloading: " + name + version + ".jar");
                    throw new FileNotFoundException("Error downloading: " + name + version + ".jar");
                }
            }
        }
    }

    /**
     * Returns the current version of a type
     *
     * @param type  The version type
     * @return      The version string
     */
    private static String getVersion(String type) {
        switch (type) {
            case "Vanilla":
                return vanillaVersion;
            case "Snapshot":
                return snapshotVersion;
            case "Bungee":
                return bungeeVersion;
            default:
                return "";
        }
    }

    /**
     * Sets a server type's last downloaded version.
     *
     * @param type      The version type
     * @param version   The version string
     */
    private static void setVersion(String type, String version) {
        if (!type.equals("")) {
            switch (type) {
                case "Vanilla":
                    vanillaVersion = version;
                    break;
                case "Snapshot":
                    snapshotVersion = version;
                    break;
                case "Bungee":
                    bungeeVersion = version;
            }
        }
    }
}