package xyz.thastertyn.Config;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.naming.ConfigurationException;

import xyz.thastertyn.Types.Credentials;

/**
 * Config parser for key value pairs with support for # comments
 */
public class Config {

	private String path;

	private File configFile;
	private File configPath;

	private boolean configExists = false;
	private boolean isDataLoaded = false;
	
	private boolean hasReadFailed = false;
	private boolean hasWriteFailed = false;

	private HashMap<String, String> configData = new HashMap<>();

	private static Config config = new Config();

	//Tries finding appropriate path to the file
	public Config()
	{
		if(System.getProperty("os.name").equals("Linux"))
		{
			// /home/user/.local/share/jecnak/...
			path = System.getProperty("user.home") + "/.config/jecnak/";
		}else if(System.getProperty("os.name").contains("Windows"))
		{
			// C:\Users\\user\AppData\Roaming\...
			path = System.getenv("APPDATA") + "\\jecnak\\";
		}else{
			configExists = false;
		}

		configPath = new File(path);
		configFile = new File(configPath, "config.conf");

		if(!configFile.exists())
		{
			createConfigFile();
		}
		
		configExists = true;

		try{
			readConfig();
		}catch(ConfigurationException e)
		{
			hasReadFailed = true;
		}
	}

	/**
	 * When no file exists, it is created together with it's directory
	 * @return true or false whether the action succeeded. {@link #hasWriteFailed} is set to true if false is returned
	 */
	private boolean createConfigFile()
	{
		try{
			configPath.mkdirs();
			configFile.createNewFile();
			return true;
		}catch(IOException e)
		{
			hasWriteFailed = true;
			return false;
		}
	}
	
	/**
	 * Tries reading the config file and parse it into {@link #configData}
	 * @throws ConfigurationException when a syntax error in the config is found, i.e. "user=" missing the value or just random gibberish
	 */
	private void readConfig() throws ConfigurationException
	{
		try {
			BufferedReader reader = new BufferedReader(new FileReader(configFile));

			String line = "";
			int lineNum = 1;

			while((line = reader.readLine()) != null)
			{
				if(line.matches("^\\s*#.*"))
				{
					configData.put("comment" + lineNum, line);
				}else{
					String[] data = line.split("=");

					// Check if key or value are null or emtpy and if yes, call that a syntax error
					if((data[0] == null || data[0].trim().isBlank()) || (data[1] == null ||data[1].trim().isBlank()))
					{
						reader.close();
						hasReadFailed = true;
						throw new ConfigurationException("Syntax Error on line " + lineNum + "\n" + "-> " + line);
					}else{
						configData.put(data[0], data[1]);
					}
				}
				lineNum++;
			}

			isDataLoaded = true;
			reader.close();
		} catch (IOException e) {
			hasReadFailed = true;
			e.printStackTrace();
		}
	}

	/**
	 * Checks if a key is stored in {@link #configData}
	 * @param key String key
	 * @return true or false if it exists or not
	 */
	public boolean checkIfExists(String key)
	{
		return configData.containsKey(key);
	}

	/**
	 * Shortcut for checking credentials specifically. Just runs {@link #checkIfExists(String)} for user and pass keys
	 * @return true or false if they exist or not
	 */
	public boolean checkIfCredentialsExist()
	{
		return (checkIfExists("user") && checkIfExists("pass"));
	}
	
	/**
	 * Tries getting the credentials, should be combined with {@link #checkIfCredentialsExist()} to assure no null values are returned
	 * @return {@link Credentials} containing the username and password
	 * @throws ConfigurationException when {@link #readConfig()} fails to read the config file, meaning nothing is loaded at all. Basically force the check if nothing is returned.
	 */
	public Credentials getCredentials() throws ConfigurationException
	{
		return new Credentials(getConfig("user"), getConfig("pass"));
	}

	/**
	 * Gets a specific key from {@link #configData}
	 * @param key to get
	 * @return value assigned to the key
	 * @throws ConfigurationException when {@link #readConfig()} fails to read to force addressing the possibility of no data
	 */
	public String getConfig(String key) throws ConfigurationException
	{
		if(!configExists)
		{
			return "";
		}

		if(!isDataLoaded)
		{
			readConfig();
		}

		if(!hasReadFailed)
		{
			String value = configData.get(key);
			return (value == null) ? "" : value;
		}else{
			return "";
		}
	}

	/**
	 * Adds a key and value to {@link #configData}
	 * @param key
	 * @param value
	 * @return {@code this} for the ability to chain these methods
	 */
	public Config addConfig(String key, String value)
	{
		configData.put(key, value);
		return this;
	}

	/**
	 * Removes a key from {@link #configData}
	 * @param key
	 * @return {@code this} for the ability to chain these methods
	 */
	public Config removeConfig(String key)
	{
		configData.remove(key);
		return this;
	}

	/**
	 * Removes credentials from {@link #configData}
	 * @return {@code this} for the ability to chain these methods
	 */
	public Config removeCredentials()
	{
		configData.remove("user");
		configData.remove("pass");
		return this;
	}

	/**
	 * Tries writting current state of {@link #configData} to {@link #configFile}
	 * @return true or false whether it succeeded or not
	 */
	public boolean writeConfig()
	{
		if(hasWriteFailed)
		{
			return false;
		}

		if(!configFile.exists())
		{
			createConfigFile();
		}

		StringBuilder finalString = new StringBuilder();

		for(Map.Entry<String, String> entry : configData.entrySet())
		{
			if(entry.getKey().matches("comment[0-9]+"))
			{
				finalString.append(entry.getValue() + "\n");
			}else{
				finalString.append(entry.getKey() + "=" + entry.getValue() + "\n");
			}
		}

		try {
			BufferedWriter writer = new BufferedWriter(new FileWriter(configFile));

			writer.write(finalString.toString());

			writer.close();
			return true;
		} catch (IOException e) {
			e.printStackTrace();
			hasWriteFailed = true;
			return false;
		}
	}


	public String getPath()
	{
		return configFile.getAbsolutePath();
	}

	/**
	 * Singleton
	 * @return {@link #config}
	 */
	public static Config getInstance()
	{
		return config;
	}
}
