package com.jscape.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Properties;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
@SuppressObfuscation
public class ConfigurationProperties {

    private final Properties base;

    public static ConfigurationProperties readValue(InputStream in)
            throws IOException {
        Properties base = IOTools.readProperties(in, null);
        return new ConfigurationProperties(base);
    }

    public static void writeValue(ConfigurationProperties properties, OutputStream out)
            throws IOException {
        IOTools.writeProperties(properties.base, "", out);
    }

    public static ConfigurationProperties forFile(Path file)
            throws IOException {
        Properties base = IOTools.readPropertiesFile(file.toFile(), null);
        return new ConfigurationProperties(base);
    }

    public static ConfigurationProperties forFile(File file)
            throws IOException {
        return forFile(file.toPath());
    }

    public static void toFile(ConfigurationProperties properties, Path file)
            throws IOException {
        IOTools.writePropertiesFile(properties.base, "", file.toFile());
    }

    public static void toFile(ConfigurationProperties properties, File file)
            throws IOException {
        toFile(properties, file.toPath());
    }

    public ConfigurationProperties(Properties base) {
        assertNotNull(base);
        this.base = base;
    }

    public ConfigurationProperties() {
        this(new Properties());
    }

    public Properties asProperties() {
        return this.base;
    }

    public ConfigurationProperties put(String key, Object value) {
        this.base.setProperty(key, value.toString());
        return this;
    }

    public <T> ConfigurationProperties put(String key, T value, Format<T> format) {
        String data = format.format(value);
        this.base.setProperty(key, data);
        return this;
    }

    public String stringValueOf(String key, String defaultValue) {
        String value = this.base.getProperty(key);
        return value != null ? value : defaultValue;
    }

    public String stringValueOf(String key)
            throws UnsupportedValueException {
        String value = this.base.getProperty(key);

        if (value == null) {
            throw new UnsupportedValueException(String.format("Required string value for key '%s' is not found.", key));
        }

        return value;
    }

    public Boolean booleanValueOf(String key, Boolean defaultValue) {
        String data = this.base.getProperty(key);
        return data != null ? Boolean.valueOf(data) : defaultValue;
    }

    public Boolean booleanValueOf(String key)
            throws UnsupportedValueException {
        String data = this.base.getProperty(key);

        if (data == null) {
            throw new UnsupportedValueException(String.format("Required boolean value for key '%s' is not found.", key));
        }

        return Boolean.valueOf(data);
    }

    public Integer intValueOf(String key, Integer defaultValue) {
        String data = this.base.getProperty(key);

        try {
            return Integer.valueOf(data);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    public Integer intValueOf(String key)
            throws UnsupportedValueException {
        String data = this.base.getProperty(key);

        if (data == null) {
            throw new UnsupportedValueException(String.format("Required integer value for key '%s' is not found.", key));
        }

        try {
            return Integer.valueOf(data);
        } catch (NumberFormatException e) {
            throw new UnsupportedValueException(String.format("Required integer value for key '%s' has unsupported format.", key));
        }
    }

    public Long longValueOf(String key, Long defaultValue) {
        String data = this.base.getProperty(key);

        try {
            return Long.valueOf(data);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    public Long longValueOf(String key)
            throws UnsupportedValueException {
        String data = this.base.getProperty(key);

        if (data == null) {
            throw new UnsupportedValueException(String.format("Required long value for key '%s' is not found.", key));
        }

        try {
            return Long.valueOf(data);
        } catch (NumberFormatException e) {
            throw new UnsupportedValueException(String.format("Required long value for key '%s' has unsupported format.", key));
        }
    }

    public <T> T valueOf(String key, Format<T> format, T defaultValue) {
        String data = this.base.getProperty(key);

        try {
            return format.parse(data);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    public <T> T valueOf(String key, Format<T> format)
            throws UnsupportedValueException {
        String data = this.base.getProperty(key);

        if (data == null) {
            throw new UnsupportedValueException(String.format("Required value for key '%s' is not found.", key));
        }

        try {
            return format.parse(data);
        } catch (Exception e) {
            throw new UnsupportedValueException(String.format("Required value for key '%s' has unsupported format. %s", key, e.getMessage() != null ? e.getMessage() : ""));
        }
    }

    @Override
    public String toString() {
        return "ConfigurationProperties {"
                + "base=" + this.base
                + '}';
    }

    @SuppressObfuscation
    public static class UnsupportedValueException
            extends IOException {

        public UnsupportedValueException(String message) {
            super(message);
        }
    }

    @SuppressObfuscation
    public interface Format<T> {

        String format(T value);

        T parse(String data)
                throws Exception;

    }

    @SuppressWarnings("unchecked")
    @SuppressObfuscation
    public static class EnumFormat<E extends Enum>
            implements Format<E> {

        private final Class<E> enumClass;

        public EnumFormat(Class<E> enumClass) {
            assertNotNull(enumClass);
            this.enumClass = enumClass;
        }

        @Override
        public String format(E value) {
            return value.name();
        }

        @Override
        public E parse(String data)
                throws Exception {
            return (E) Enum.<E>valueOf(this.enumClass, data);
        }
    }
}
