package org.xbib.settings.common;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.xbib.datastructures.api.ByteSizeValue;
import org.xbib.datastructures.api.TimeValue;
import org.xbib.settings.Settings;
import org.xbib.settings.SettingsException;

public class BaseSettings implements Settings {

    private static final String[] EMPTY_ARRAY = new String[0];

    private final Map<String, String> map;

    BaseSettings(Map<String, String> map) {
        this.map = map;
    }

    public static BaseSettingsBuilder builder() {
        return new BaseSettingsBuilder();
    }

    public static Settings fromMap(Map<String, Object> map) {
        BaseSettingsBuilder builder = new BaseSettingsBuilder();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            builder.put(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : null);
        }
        return builder.build();
    }

    public static void toMap(Settings settings, Map<String, Object> map) {
        for (String key : settings.getAsMap().keySet()) {
            map.put(key, settings.get(key));
        }
    }

    public static String[] splitStringByCommaToArray(String s) {
        return splitStringToArray(s, ',');
    }

    public static String[] splitStringToArray(String s, char c) {
        if (s.isEmpty()) {
            return EMPTY_ARRAY;
        }
        final char[] chars = s.toCharArray();
        int count = 1;
        for (final char x : chars) {
            if (x == c) {
                count++;
            }
        }
        final String[] result = new String[count];
        final int len = chars.length;
        int start = 0;
        int pos = 0;
        int i = 0;
        for (; pos < len; pos++) {
            if (chars[pos] == c) {
                int size = pos - start;
                if (size > 0) {
                    result[i++] = new String(chars, start, size);
                }
                start = pos + 1;
            }
        }
        int size = pos - start;
        if (size > 0) {
            result[i++] = new String(chars, start, size);
        }
        if (i != count) {
            String[] result1 = new String[i];
            System.arraycopy(result, 0, result1, 0, i);
            return result1;
        }
        return result;
    }

    @Override
    public Map<String, String> getAsMap() {
        return this.map;
    }

    @Override
    public Map<String, Object> getAsStructuredMap() {
        Map<String, Object> stringObjectMap = new LinkedHashMap<>();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            processSetting(stringObjectMap, "", key, value);
        }
        for (Map.Entry<String, Object> entry : stringObjectMap.entrySet()) {
            String key = entry.getKey();
            Object object = entry.getValue();
            if (object instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String, Object> valMap = (Map<String, Object>) object;
                stringObjectMap.put(key, convertMapsToArrays(valMap));
            }
        }
        return stringObjectMap;
    }

    @Override
    public Settings getByPrefix(String prefix) {
        BaseSettingsBuilder builder = new BaseSettingsBuilder();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (key.startsWith(prefix)) {
                if (key.length() < prefix.length()) {
                    continue;
                }
                builder.put(key.substring(prefix.length()), value);
            }
        }
        return builder.build();
    }

    @Override
    public Settings getAsSettings(String setting) {
        return getByPrefix(setting + ".");
    }

    @Override
    public boolean containsSetting(String setting) {
        if (map.containsKey(setting)) {
            return true;
        }
        for (String key : map.keySet()) {
            if (key.startsWith(setting)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public String get(String setting) {
        return map.get(setting);
    }

    @Override
    public String get(String setting, String defaultValue) {
        String s = map.get(setting);
        return s == null ? defaultValue : s;
    }

    @Override
    public float getAsFloat(String setting, float defaultValue) {
        String s = get(setting);
        try {
            return s == null ? defaultValue : Float.parseFloat(s);
        } catch (NumberFormatException e) {
            throw new SettingsException("Failed to parse float setting [" + setting + "] with value [" + s + "]", e);
        }
    }

    @Override
    public double getAsDouble(String setting, double defaultValue) {
        String s = get(setting);
        try {
            return s == null ? defaultValue : Double.parseDouble(s);
        } catch (NumberFormatException e) {
            throw new SettingsException("Failed to parse double setting [" + setting + "] with value [" + s + "]", e);
        }
    }

    @Override
    public int getAsInt(String setting, int defaultValue) {
        String s = get(setting);
        try {
            return s == null ? defaultValue : Integer.parseInt(s);
        } catch (NumberFormatException e) {
            throw new SettingsException("Failed to parse int setting [" + setting + "] with value [" + s + "]", e);
        }
    }

    @Override
    public long getAsLong(String setting, long defaultValue) {
        String s = get(setting);
        try {
            return s == null ? defaultValue : Long.parseLong(s);
        } catch (NumberFormatException e) {
            throw new SettingsException("Failed to parse long setting [" + setting + "] with value [" + s + "]", e);
        }
    }

    @Override
    public boolean getAsBoolean(String setting, boolean defaultValue) {
        String value = get(setting);
        if (value == null) {
            return defaultValue;
        }
        return !("false".equals(value) || "0".equals(value) || "off".equals(value) || "no".equals(value));
    }

    @Override
    public TimeValue getAsTime(String setting, TimeValue defaultValue) {
        return TimeValue.parseTimeValue(get(setting), defaultValue);
    }

    @Override
    public ByteSizeValue getAsBytesSize(String setting, ByteSizeValue defaultValue) {
        return ByteSizeValue.parseBytesSizeValue(get(setting), defaultValue);
    }

    @Override
    public String[] getAsArray(String settingPrefix) {
        return getAsArray(settingPrefix, EMPTY_ARRAY);
    }

    @Override
    public String[] getAsArray(String settingPrefix, String[] defaultArray) {
        List<String> result = new ArrayList<>();
        if (get(settingPrefix) != null) {
            String[] strings = splitStringByCommaToArray(get(settingPrefix));
            if (strings.length > 0) {
                for (String string : strings) {
                    result.add(string.trim());
                }
            }
        }
        int counter = 0;
        while (true) {
            String value = get(settingPrefix + '.' + (counter++));
            if (value == null) {
                break;
            }
            result.add(value.trim());
        }
        if (result.isEmpty()) {
            return defaultArray;
        }
        return result.toArray(new String[0]);
    }

    @Override
    public Map<String, Settings> getGroups(String prefix) {
        String settingPrefix = prefix;
        if (settingPrefix.charAt(settingPrefix.length() - 1) != '.') {
            settingPrefix = settingPrefix + ".";
        }
        // we don't really care that it might happen twice
        Map<String, Map<String, String>> hashMap = new LinkedHashMap<>();
        for (String o : this.map.keySet()) {
            if (o.startsWith(settingPrefix)) {
                String nameValue = o.substring(settingPrefix.length());
                int dotIndex = nameValue.indexOf('.');
                if (dotIndex == -1) {
                    throw new SettingsException("failed to get setting group for ["
                            + settingPrefix
                            + "] setting prefix and setting [" + o + "] because of a missing '.'");
                }
                String name = nameValue.substring(0, dotIndex);
                String value = nameValue.substring(dotIndex + 1);
                Map<String, String> groupSettings = hashMap.computeIfAbsent(name, k -> new LinkedHashMap<>());
                groupSettings.put(value, get(o));
            }
        }
        Map<String, Settings> retVal = new LinkedHashMap<>();
        for (Map.Entry<String, Map<String, String>> entry : hashMap.entrySet()) {
            String key = entry.getKey();
            Map<String, String> value = entry.getValue();
            retVal.put(key, new BaseSettings(value));
        }
        return retVal;
    }

    @Override
    public boolean equals(Object o) {
        return this == o || !(o == null || getClass() != o.getClass()) && map.equals(((BaseSettings) o).map);
    }

    @Override
    public int hashCode() {
        return map.hashCode();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public void close() throws IOException {
        // do nothing
    }

    private void processSetting(Map<String, Object> map, String prefix, String setting, String value) {
        int prefixLength = setting.indexOf('.');
        if (prefixLength == -1) {
            @SuppressWarnings("unchecked")
            Map<String, Object> innerMap = (Map<String, Object>) map.get(prefix + setting);
            if (innerMap != null) {
                for (Map.Entry<String, Object> e : innerMap.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    map.put(prefix + setting + "." + k, v);
                }
            }
            map.put(prefix + setting, value);
        } else {
            String key = setting.substring(0, prefixLength);
            String rest = setting.substring(prefixLength + 1);
            Object existingValue = map.get(prefix + key);
            if (existingValue == null) {
                Map<String, Object> newMap = new LinkedHashMap<>();
                processSetting(newMap, "", rest, value);
                map.put(key, newMap);
            } else {
                if (existingValue instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> innerMap = (Map<String, Object>) existingValue;
                    processSetting(innerMap, "", rest, value);
                    map.put(key, innerMap);
                } else {
                    processSetting(map, prefix + key + ".", rest, value);
                }
            }
        }
    }

    private Object convertMapsToArrays(Map<String, Object> map) {
        if (map.isEmpty()) {
            return map;
        }
        boolean isArray = true;
        int maxIndex = -1;
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (isArray) {
                try {
                    int index = Integer.parseInt(key);
                    if (index >= 0) {
                        maxIndex = Math.max(maxIndex, index);
                    } else {
                        isArray = false;
                    }
                } catch (NumberFormatException ex) {
                    isArray = false;
                }
            }
            if (value instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String, Object> valMap = (Map<String, Object>) value;
                map.put(key, convertMapsToArrays(valMap));
            }
        }
        if (isArray && (maxIndex + 1) == map.size()) {
            ArrayList<Object> newValue = new ArrayList<>(maxIndex + 1);
            for (int i = 0; i <= maxIndex; i++) {
                Object obj = map.get(Integer.toString(i));
                if (obj == null) {
                    return map;
                }
                newValue.add(obj);
            }
            return newValue;
        }
        return map;
    }
}
