package me.neznamy.tab.shared;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.parser.ParserException;
import org.yaml.snakeyaml.scanner.ScannerException;

import com.google.common.collect.Lists;

import me.neznamy.tab.shared.placeholders.Placeholder;
import me.neznamy.tab.shared.placeholders.Placeholders;

@SuppressWarnings("unchecked")
public class ConfigurationFile{
	
	public static final File dataFolder = new File("plugins" + File.separatorChar + "TAB");
	
	private File file;
	private Yaml yaml;
	private List<String> header;
	private Map<String, Object> values;
	
	public ConfigurationFile(String source, String destination, List<String> header) throws Exception{
		FileInputStream input = null;
		try {
			this.header = header;
			dataFolder.mkdirs();
			file = new File(dataFolder, destination);
			if (!file.exists()) Files.copy(getClass().getClassLoader().getResourceAsStream("resources/" + source), file.toPath());
			DumperOptions options = new DumperOptions();
			options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
			yaml = new Yaml(options);
			input = new FileInputStream(file);
			values = yaml.load(new InputStreamReader(input, Charset.forName("UTF-8")));
			if (values == null) values = new HashMap<String, Object>();
			input.close();
			Shared.mainClass.convertConfig(this);
			if (!hasHeader()) fixHeader();
			Placeholders.findAllUsed(values);
		} catch (ParserException | ScannerException e) {
			input.close();
			Shared.errorManager.startupWarn("File " + destination + " has broken formatting.");
			Shared.brokenFile = file.getPath();
			Shared.mainClass.sendConsoleMessage("&6[TAB] Error message from yaml parser: " + e.getMessage());
			String fix = Shared.errorManager.suggestYamlFix(e, readAllLines());
			if (fix != null) {
				Shared.mainClass.sendConsoleMessage("&d[TAB] Suggestion: " + fix);
			}
			throw e;
		}
	}
	public ConfigurationFile(String sourceAndDestination, List<String> header) throws Exception{
		this(sourceAndDestination, sourceAndDestination, header);
	}
	public String getName() {
		return file.getName();
	}
	public Map<String, Object> getValues(){
		return values;
	}
	public Object getObject(String path) {
		return getObject(path, null);
	}
	public Object getObject(String path, Object defaultValue) {
		try {
			Object value = values;
			for (String tab : path.split("\\.")) {
				tab = tab.replace("@#@", ".");
				value = getIgnoreCase((Map<String, Object>) value, tab);
			}
			if (value == null && defaultValue != null) {
				set(path, defaultValue);
				return defaultValue;
			}
			return value;
		} catch (Throwable e) {
			if (defaultValue != null) set(path, defaultValue);
			return defaultValue;
		}
	}
	private Object getIgnoreCase(Map<String, Object> map, String key) {
		for (String mapkey : map.keySet()) {
			if (mapkey.equalsIgnoreCase(key)) return map.get(mapkey);
		}
		return null;
	}
	public String getString(String path) {
		return getString(path, null);
	}
	public String getString(String path, String defaultValue) {
		Object value = getObject(path, defaultValue);
		if (value == null) return defaultValue;
		return value+"";
	}
	public List<String> getStringList(String path) {
		return getStringList(path, null);
	}
	public List<String> getStringList(String path, List<String> defaultValue) {
		Object value = getObject(path, defaultValue);
		if (value == null) return defaultValue;
		if (!(value instanceof List)) {
			dataMismatch(path, "ArrayList", value.getClass().getSimpleName());
			return new ArrayList<String>();
		}
		List<String> fixedList = new ArrayList<String>();
		for (Object key : (List<Object>)value) {
			fixedList.add(key+"");
		}
		return fixedList;
	}
	
	
	public boolean hasConfigOption(String path) {
		return getObject(path) != null;
	}
	
	public Integer getInt(String path) {
		return getInt(path, null);
	}
	public Integer getInt(String path, Integer defaultValue) {
		Object value = getObject(path, defaultValue);
		if (value == null) return defaultValue;
		try{
			return Integer.parseInt(value+"");
		} catch (Exception e) {
			dataMismatch(path, "Integer", value.getClass().getSimpleName());
			return defaultValue;
		}
	}
	
	
	public Boolean getBoolean(String path) {
		return getBoolean(path, null);
	}
	public Boolean getBoolean(String path, Boolean defaultValue) {
		Object value = getObject(path, defaultValue);
		if (value == null) return defaultValue;
		try{
			return Boolean.parseBoolean(value+"");
		} catch (Exception e) {
			dataMismatch(path, "Boolean", value.getClass().getSimpleName());
			return defaultValue;
		}
	}
	
	
	public Double getDouble(String path, double defaultValue) {
		Object value = getObject(path, defaultValue);
		if (value == null) return defaultValue;
		try{
			return Double.parseDouble(value+"");
		} catch (Exception e) {
			dataMismatch(path, "Double", value.getClass().getSimpleName());
			return defaultValue;
		}
	}
	
	@SuppressWarnings("rawtypes")
	public Map getConfigurationSection(String path) {
		if (path == null || path.length() == 0) return values;
		Object value = getObject(path, null);
		if (value == null) return new HashMap<>();
		if (value instanceof Map) {
			return (Map) value;
		} else {
			dataMismatch(path, "Map", value.getClass().getSimpleName());
			return new HashMap<>();
		}
	}
	
	
	private void dataMismatch(String path, String expected, String found) {
		Shared.errorManager.startupWarn("Data mismatch in &e" + file.getName() + "&c. Value of &e" + path + "&c is expected to be &e" + expected + "&c, but is &e" + found + "&c. This is a misconfiguration issue.");
	}
	public void set(String path, Object value) {
		set(values, path, value);
		save();
	}
	private Map<String, Object> set(Map<String, Object> map, String path, Object value) {
		if (path.contains(".")) {
			String keyWord = fixKey(map, path.split("\\.")[0]);
			Object submap = map.get(keyWord);
			if (submap == null || !(submap instanceof Map)) {
				submap = new HashMap<String, Object>();
			}
			map.put(keyWord.replace("@#@", "."), set((Map<String, Object>) submap, path.substring(keyWord.length()+1, path.length()), value));
		} else {
			if (value == null) {
				map.remove(path);
			} else {
				map.put(path, value);
			}
		}
		return map;
	}
	private String fixKey(Map<?, ?> map, String key) {
		for (Entry<?, ?> e : map.entrySet()) {
			if (e.getKey().toString().equalsIgnoreCase(key)) return e.getKey().toString();
		}
		return key;
	}
	public void save() {
		try {
			Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
			yaml.dump(values, writer);
			writer.close();
			if (!hasHeader()) fixHeader();
		} catch (Throwable e) {
			Shared.errorManager.criticalError("Failed to save yaml file " + file.getPath(), e);
		}
	}
	public boolean hasHeader() {
		if (header == null) return true;
		for (String line : readAllLines()) {
			if (line.contains("#")) return true;
		}
		return false;
	}
	public void fixHeader() {
		if (header == null) return;
		try {
			List<String> content = Lists.newArrayList(header);
			content.addAll(readAllLines());
			file.delete();
			file.createNewFile();
			BufferedWriter buf = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8"));
			for (String line : content) {
				buf.write(line + System.getProperty("line.separator"));
			}
			buf.close();
		} catch (Exception ex) {
			Shared.errorManager.criticalError("Failed to modify file " + file, ex);
		}
	}
	private List<String> readAllLines() {
		List<String> list = new ArrayList<String>();
		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
			String line;
			while ((line = br.readLine()) != null) {
				list.add(line);
			}
			br.close();
		} catch (Exception ex) {
			Shared.errorManager.criticalError("Failed to read file " + file, ex);
		}
		return list;
	}
	public Set<String> getUsedPlaceholderIdentifiersRecursive(String... simpleKeys){
		Set<String> base = getUsedPlaceholders(values, simpleKeys);
		for (String placeholder : base.toArray(new String[0])) {
			List<Placeholder> pl = Placeholders.detectPlaceholders(placeholder);
			for (Placeholder p : pl) {
				base.add(p.getIdentifier());
			}
		}
		return base;
	}
	private Set<String> getUsedPlaceholders(Map<String, Object> map, String... simpleKeys){
		Set<String> values = new HashSet<String>();
		for (Entry<String, Object> entry : map.entrySet()) {
			for (String simpleKey : simpleKeys) {
				if (entry.getKey().equals(simpleKey)) values.addAll(Placeholders.detectAll(String.valueOf(entry.getValue())));
			}
			if (entry.getValue() instanceof Map) {
				values.addAll(getUsedPlaceholders((Map<String, Object>)entry.getValue(), simpleKeys));
			}
			if (entry.getValue() instanceof List) {
				for (Object obj : (List<Object>)entry.getValue()) {
					for (String simpleKey : simpleKeys) {
						if (String.valueOf(obj).equals(simpleKey)) values.addAll(Placeholders.detectAll(String.valueOf(entry.getValue())));
					}
				}
			}
		}
		return values;
	}
}