/**
 *
 */
package io.sinistral.proteus.modules;

import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.util.Types;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.List;
import java.util.Map.Entry;

/**
 * Much of this is taken with reverence from Jooby
 *
 * @author jbauer
 */
@Singleton
public class ConfigModule extends AbstractModule
{
    private static Logger log = LoggerFactory.getLogger(ConfigModule.class.getCanonicalName());

    protected String configFile = null;
    protected URL configURL = null;
    protected Config config = null;

    public ConfigModule()
    {
        this.configFile = System.getProperty("config.file");

        if (this.configFile == null) {
            this.configFile = "application.conf";
        }
    }



    public ConfigModule(String configFile)
    {
        this.configFile = configFile;
    }

    public ConfigModule(URL configURL)
    {
        this.configURL = configURL;
    }

    public Config getConfig()
    {
        return config;
    }

    public void setConfig(Config config)
    {
        this.config = config;
    }

    @SuppressWarnings("unchecked")
    private void bindConfig(final Config config)
    {
        traverse(this.binder(), "", config.root());

        for (Entry<String, ConfigValue> entry : config.entrySet()) {
            String name = entry.getKey();
            Named named = Names.named(name);
            Object value = entry.getValue().unwrapped();

            if (value instanceof List) {
                List<Object> values = (List<Object>) value;
                Type listType = (values.size() == 0)
                        ? String.class
                        : Types.listOf(values.iterator().next().getClass());
                Key<Object> key = (Key<Object>) Key.get(listType, Names.named(name));

                this.binder().bind(key).toInstance(values);
            } else {
                this.binder().bindConstant().annotatedWith(named).to(value.toString());
            }
        }

        Config referenceConfig = ConfigFactory.load(ConfigFactory.defaultReference());

        this.config = ConfigFactory.load(config).withFallback(referenceConfig);

        log.debug(this.config.toString());

        this.binder().bind(Config.class).toInstance(config);
    }

    @Override
    protected void configure()
    {
        Config config = ConfigFactory.defaultApplication();
        Config referenceConfig = ConfigFactory.load(ConfigFactory.defaultReference());

        config = ConfigFactory.load(config).withFallback(referenceConfig);

        if (configURL != null) {
            config = ConfigFactory.load(ConfigFactory.parseURL(configURL)).withFallback(config);
        } else if (configFile != null) {
            config = fileConfig(configFile).withFallback(config);
        }

        this.bindConfig(config);

        install(new ApplicationModule(this.config));
    }

    private static Config fileConfig(final String fileName)
    {
        File userDirectory = new File(System.getProperty("user.dir"));
        File fileRoot = new File(userDirectory, fileName);

        if (fileRoot.exists()) {
            return ConfigFactory.load(ConfigFactory.parseFile(fileRoot));
        } else {
            File fileConfig = new File(new File(userDirectory, "conf"), fileName);

            if (fileConfig.exists()) {
                return ConfigFactory.load(ConfigFactory.parseFile(fileConfig));
            }
        }

        return ConfigFactory.empty();
    }

    private static void traverse(final Binder binder, final String nextPath, final ConfigObject rootConfig)
    {
        rootConfig.forEach(
                (key, value) -> {
                    if (value instanceof ConfigObject) {
                        try {

                            ConfigObject child = (ConfigObject) value;
                            String path = nextPath + key;

                            Named named = Names.named(path);

                            binder.bind(Config.class).annotatedWith(named).toInstance(child.toConfig());

                            traverse(binder, path + ".", child);

                        } catch (Exception e) {
                            log.error("Error binding " + value, e);
                        }
                    }
                });
    }
}