import * as Index from "./index.d";
import Utils from "./utils";

export default class Settings {

    protected static instance: Settings | null;
    private default: Index.CustomModuleOptionsStrict = require("../options.json");
    private enviroments: Index.CustomModuleOptions;
    private envMap = require("../envMap");

    private swagger: {[key: string]: Index.SwaggerOptions} = {};
    private morgan: {[key: string]: Index.SwaggerOptions} = {};
    private zipkin: {[key: string]: Index.ZipkinOptions} = {};
    private crashReporter: {[key: string]: Index.CrashReporterOptions} = {};
    private mailer: {[key: string]: Index.MailerOptions} = {};
    private logger: {[key: string]: Index.LoggerOptions} = {};
    private redis: {[key: string]: Index.RedisOptions} = {};
    private cassandra: {[key: string]: Index.CassandraOptions} = {};
    private elastic: {[key: string]: Index.ElasticOptions} = {};
    private mysql: {[key: string]: Index.MysqlOptions} = {};    
    private metrics: {[key: string]: Index.MetricsOptions} = {};
    private routeMetrics: {[key: string]: Index.RouteMetricsOptions} = {};    
    private consul: {[key: string]: Index.ConsulOptions} = {};    
    private custom: {[key: string]: any} = {};
    public static ZIPKIN_CONTEXT_NAME: string = 'express-context';;

    constructor(){
        this.enviroments = this.extractEnviroments();
    }
    

    public static get Instance() {
        if (this.instance === null || this.instance === undefined) {
            this.instance = new this();
        }
        return this.instance;
    }

    public set(settings: Index.ModuleOptions){
        this.default = Utils.deepExtend({}, this.default, settings);
        this.enviroments = this.extractEnviroments();
    }

    public setFile(filename: string): boolean{
        let path = Utils.getFullRouteFrom(filename);
        let env = Utils.readFromFile(path);
        if(env == null) return false;

        this.set(env);
        return true;
    }

    public setEnvMap(envMap: Object){
        this.envMap = Utils.deepExtend({}, this.envMap, envMap);
        this.enviroments = this.extractEnviroments();
    }

    public setEnvMapFile(filename: string): boolean{
        let envMap = Utils.readFromFile(filename);
        if(envMap == null) return false;

        this.setEnvMap(envMap);
        return true;
    }


    /**
     * Setters
     */
    public setRouteMetrics(token: string, settings: Index.RouteMetricsOptions){
        let env: Index.RouteMetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.routeMetrics != null ? this.enviroments.routeMetrics : null;
        this.routeMetrics[token] = this.applySetting<Index.RouteMetricsOptions>(settings,  this.routeMetrics[token], this.default.routeMetrics, env);
    }

    public setSwagger(token: string, settings: Index.SwaggerOptions | null){
        let env: Index.SwaggerOptions | null = token == "global" && this.enviroments != null && this.enviroments.swagger != null ? this.enviroments.swagger : null;
        this.swagger[token] = this.applySetting<Index.SwaggerOptions>(settings,  this.swagger[token], this.default.swagger, env);
    }

    public setMorgan(token: string, settings: Index.MorganOptions | null){
        let env: Index.MorganOptions | null = token == "global" && this.enviroments != null && this.enviroments.morgan != null ? this.enviroments.morgan : null;        
        this.morgan[token] = this.applySetting<Index.MorganOptions>(settings,  this.morgan[token], this.default.morgan, env);
    }

    public setZipkin(token: string, settings: Index.ZipkinOptions | null){
        let env: Index.ZipkinOptions | null = token == "global" && this.enviroments != null && this.enviroments.zipkin != null ? this.enviroments.zipkin : null;        
        this.zipkin[token] = this.applySetting<Index.ZipkinOptions>(settings,  this.zipkin[token], this.default.zipkin, env);
    }

    public setCrashReporter(token: string, settings: Index.CrashReporterOptions){
        let env: Index.CrashReporterOptions | null = token == "global" && this.enviroments != null && this.enviroments.crashReporter != null ? this.enviroments.crashReporter : null;        
        this.crashReporter[token] = this.applySetting<Index.CrashReporterOptions>(settings,  this.crashReporter[token], this.default.crashReporter, env);
    }

    public setMailer(token: string, settings: Index.MailerOptions){
        let env: Index.MailerOptions | null = token == "global" && this.enviroments != null && this.enviroments.mailer != null ? this.enviroments.mailer : null;        
        this.mailer[token] = this.applySetting<Index.MailerOptions>(settings,  this.mailer[token], this.default.mailer, env);
    }

    public setLogger(token: string, settings: Index.LoggerOptions){
        let env: Index.LoggerOptions | null = token == "global" && this.enviroments != null && this.enviroments.logger != null ? this.enviroments.logger : null;        
        this.logger[token] = this.applySetting<Index.LoggerOptions>(settings,  this.logger[token], this.default.logger, env);
    }

    public setRedis(token: string, settings: Index.RedisOptions){
        let env: Index.RedisOptions | null = token == "global" && this.enviroments != null && this.enviroments.redis != null ? this.enviroments.redis : null;        
        this.redis[token] = this.applySetting<Index.RedisOptions>(settings,  this.redis[token], this.default.redis, env);
    }

    public setCassandra(token: string, settings: Index.CassandraOptions){
        let env: Index.CassandraOptions | null = token == "global" && this.enviroments != null && this.enviroments.cassandra != null ? this.enviroments.cassandra : null;        
        this.cassandra[token] = this.applySetting<Index.CassandraOptions>(settings,  this.cassandra[token], this.default.cassandra, env);
    }

    public setElastic(token: string, settings: Index.ElasticOptions){
        let env: Index.ElasticOptions | null = token == "global" && this.enviroments != null && this.enviroments.elastic != null ? this.enviroments.elastic : null;        
        this.elastic[token] = this.applySetting<Index.ElasticOptions>(settings,  this.elastic[token], this.default.elastic, env);
    }

    public setMysql(token: string, settings: Index.MysqlOptions){
        let env: Index.MysqlOptions | null = token == "global" && this.enviroments != null && this.enviroments.mysql != null ? this.enviroments.mysql : null;        
        this.mysql[token] = this.applySetting<Index.MysqlOptions>(settings,  this.mysql[token], this.default.mysql, env);
    }

    public setMetrics(token: string, settings: Index.MetricsOptions){
        let env: Index.MetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.metrics != null ? this.enviroments.metrics : null;
        this.metrics[token] = this.applySetting<Index.MetricsOptions>(settings,  this.metrics[token], this.default.metrics, env);
    }

    public setConsul(token: string, settings: Index.ConsulOptions){
        let env: Index.ConsulOptions | null = token == "global" && this.enviroments != null && this.enviroments.consul != null ? this.enviroments.consul : null;
        this.consul[token] = this.applySetting<Index.ConsulOptions>(settings,  this.consul[token], this.default.consul, env);
    }

    /**
     * For custom usage like tests
     * @param token 
     * @param settings 
     */
    public setCustom(token: string, settings: any){
        let env: Index.MetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.custom != null ? this.enviroments.custom : null;        
        this.custom[token] = this.applySetting<any>(settings,  this.custom[token], this.default.custom, env);
    }


    /**
     * Geters
     */
    public getRouteMetrics(token: string): Index.RouteMetricsOptions{
        let env: Index.RouteMetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.routeMetrics != null ? this.enviroments.routeMetrics : null;
        return this.selectSetting<Index.RouteMetricsOptions>(this.routeMetrics[token], this.default.routeMetrics, env);  
    }

    public getSwagger(token: string): Index.SwaggerOptions{
        let env: Index.SwaggerOptions | null = token == "global" && this.enviroments != null && this.enviroments.swagger != null ? this.enviroments.swagger : null;
        return this.selectSetting<Index.SwaggerOptions>(this.swagger[token], this.default.swagger, env);        
    }

    public getMorgan(token: string): Index.MorganOptions{
        let env: Index.MorganOptions | null = token == "global" && this.enviroments != null && this.enviroments.morgan != null ? this.enviroments.morgan : null;
        return this.selectSetting<Index.MorganOptions>(this.morgan[token], this.default.morgan, env);        
    }

    public getZipkin(token: string): Index.ZipkinOptions{
        let env: Index.ZipkinOptions | null = token == "global" && this.enviroments != null && this.enviroments.zipkin != null ? this.enviroments.zipkin : null;
        return this.selectSetting<Index.ZipkinOptions>(this.zipkin[token], this.default.zipkin, env);        
    }

    public getCrashReporter(token: string): Index.CrashReporterOptions{
        let env: Index.CrashReporterOptions | null = token == "global" && this.enviroments != null && this.enviroments.crashReporter != null ? this.enviroments.crashReporter : null;
        return this.selectSetting<Index.CrashReporterOptions>(this.crashReporter[token], this.default.crashReporter, env);  
    }

    public getMailer(token: string): Index.MailerOptions{
        let env: Index.MailerOptions | null = token == "global" && this.enviroments != null && this.enviroments.mailer != null ? this.enviroments.mailer : null;
        return this.selectSetting<Index.MailerOptions>(this.mailer[token], this.default.mailer, env);  
    }

    public getLogger(token: string): Index.LoggerOptions{
        let env: Index.LoggerOptions | null = token == "global" && this.enviroments != null && this.enviroments.logger != null ? this.enviroments.logger : null;
        return this.selectSetting<Index.LoggerOptions>(this.logger[token], this.default.logger, env);  
    }

    public getRedis(token: string): Index.RedisOptions{
        let env: Index.RedisOptions | null = token == "global" && this.enviroments != null && this.enviroments.redis != null ? this.enviroments.redis : null;
        return this.selectSetting<Index.RedisOptions>(this.redis[token], this.default.redis, env);  
    }

    public getCassandra(token: string): Index.CassandraOptions{
        let env: Index.CassandraOptions | null = token == "global" && this.enviroments != null && this.enviroments.cassandra != null ? this.enviroments.cassandra : null;
        return this.selectSetting<Index.CassandraOptions>(this.cassandra[token], this.default.cassandra, env);  
    }

    public getElastic(token: string): Index.ElasticOptions{
        let env: Index.ElasticOptions | null = token == "global" && this.enviroments != null && this.enviroments.elastic != null ? this.enviroments.elastic : null;
        return this.selectSetting<Index.ElasticOptions>(this.elastic[token], this.default.elastic, env);  
    }

    public getMysql(token: string): Index.MysqlOptions{
        let env: Index.MysqlOptions | null = token == "global" && this.enviroments != null && this.enviroments.mysql != null ? this.enviroments.mysql : null;
        return this.selectSetting<Index.MysqlOptions>(this.mysql[token], this.default.mysql, env);  
    }

    public getMetrics(token: string): Index.MetricsOptions{
        let env: Index.MetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.metrics != null ? this.enviroments.metrics : null;
        return this.selectSetting<Index.MetricsOptions>(this.metrics[token], this.default.metrics, env);  
    }

    public getConsul(token: string): Index.ConsulOptions{
        let env: Index.ConsulOptions | null = token == "global" && this.enviroments != null && this.enviroments.consul != null ? this.enviroments.consul : null;
        return this.selectSetting<Index.ConsulOptions>(this.consul[token], this.default.consul, env);  
    }

    /**
     * For custom usage like tests
     * @param token 
     */
    public getCustom(token: string): any{
        let env: Index.MetricsOptions | null = token == "global" && this.enviroments != null && this.enviroments.custom != null ? this.enviroments.custom : null;
        return this.selectSetting<any>(this.custom[token], this.default.custom, env);
    }

    public resetSwagger(token: string): void {
        this.swagger[token] = {};
    }

    public resetMorgan(token: string): void{
        this.morgan[token] = {};      
    }

    public resetZipkin(token: string): void{
        this.zipkin[token] = {};       
    }

    public resetCrashReporter(token: string): void{
        this.crashReporter[token] = {}; 
    }

    public resetMailer(token: string): void{
        this.mailer[token] = {}; 
    }

    public resetLogger(token: string): void{
        this.logger[token] = {}; 
    }

    public resetRedis(token: string): void{
        this.redis[token] = {}; 
    }

    public resetMysql(token: string): void{
        this.mysql[token] = {}; 
    }

    public resetCassandra(token: string): void{
        this.cassandra[token] = {};  
    }

    public resetElastic(token: string): void{
        this.elastic[token] = {}; 
    }

    public resetMetrics(token: string): void{
        this.metrics[token] = {}; 
    }

    /**
     * For custom usage like tests
     * @param token 
     */
    public resetCustom(token: string): void{
        this.custom[token] = {};
    }



    /**
     * Utils
     */

    private applySetting<T>(newSettings:  T | Object | null, oldSettings: T | Object | null, defaultSettings:  T | Object | null, envSettings:  T | Object | null): T{
        if(newSettings == null) newSettings = {};
        if(oldSettings == null) oldSettings = {};
        if(defaultSettings == null) defaultSettings = {};
        if(envSettings == null) envSettings = {};           
        
        return Utils.deepExtend({}, defaultSettings, oldSettings, newSettings, envSettings);
    }

    private selectSetting<T>(setting: T | null, defaultSettings: T, envSettings: T | Object | null): T{
        //If we have setting reture it
        if(setting != null) return setting;

        //Else return default settings with enviroments injected
        if(envSettings == null) envSettings = {};
        return Utils.deepExtend({}, defaultSettings, envSettings);
    }

    protected extractEnviroments(): Index.CustomModuleOptions{
        let response: any = {};
        let envNames: string[] = Object.keys(this.envMap);
        let self = this;

        envNames.forEach(function(name){
            let path: string[] = self.envMap[name].route;
            let type: string = self.envMap[name].type;
            if(path == null || type == null) return;

            path = JSON.parse(JSON.stringify(path));//Need a copy of object
            let value;
            switch(type){
                case "boolean":
                    value = self.envBoolean(name);
                break;
                case "number":
                    value = self.envNumber(name);
                break;
                case "string":
                    value = self.envString(name);
                break;
                case "array":
                    value = self.envArray(name);
                break;
                default:
                return;
            }

            let deepSize: number = path.length;
            if(deepSize < 2 || value == null) return;

            let tempParam: any = null;
            let param: any = value;

            path.reverse();//Reverse elements in array
            path.forEach(function(deep){
                tempParam = param;
                param = {};
                param[deep] = tempParam;
            });
            tempParam = null;
            
            response = Utils.deepExtend({}, response, param);
        });

        return response;
    }

    protected envArray(name: string): string[] | null {
        let response = process.env[name];
        if(response == null) return null;
        response = response.trim();                

        let array = response.match(/\S+/g);
        return array;
    }

    protected envBoolean(name: string): boolean | null {
        let response = process.env[name];
        if(response == null) return null;
        response = response.trim().toLowerCase();

        if(response == "true") return true;
        if(response == "false") return false;

        return null;
    }

    protected envString(name: string): string | null {
        let response = process.env[name];
        if(response == null) return null;
        response = response.trim();
        if(response == "") return null;

        return response;
    }

    protected envNumber(name: string): number | null {
        let response = process.env[name];
        if(response == null) return null;
        response = response.trim().toLowerCase();

        //check if string contains only digits with posible one dot in middle 
        if(!/^\d+(\.\d+)?$/.test(response)) return null;

        let number = parseFloat(response);
        if(isNaN(number)) return null;
        
        return number;
    }

    /**
     * Only for tests
     */
    public reset(){
        Settings.instance = null;
    }
}