// Package config allows one to easily specify configuration options.  It
// automatically executes the same steps for each configuration variable at
// startup:
//
//   * Set the value to the provided default.
//   * Checks if the ENV var is set; if so, override the default.
//   * Checks if the command line argument has been given; if so, override
//     the ENV variable value.
//
// The configuration parameters are documented in a map of structs; see
// ConfigParam.  The key is the name of the parameter, and the struct documents
// the type, help message, default value, and a pointer to the location to
// store the final value.  For example:
//
//   conf = config.ConfigMap {
//     "file-path": {
//       Type:  config.STRING,
//       Mesg:  "Path of file.",
//       Def:   "file.json",
//       Value: &filename,
//     },
//   }
//
// would setup configuration options with a single parameter called "file-path"
// of type string and default value "file.json".  The final configuration of
// the parameter would end up in the filename variable of type string.
//
// The conf map would be passed to Init in order to configure the system.  At
// the end of Init, all variables pointed to by the ConfigMap will be in their
// final, configured state.
package config

import (
    "os"
    "flag"
    "fmt"
    "strings"
    "strconv"
)

// Types of configuration parameters
const (
    INT = iota
    STRING
    BOOL
)

// ConfigParam defines a single configuration parameter.  Type holds the
// primitive type of the parameter, Mesg describes the parameter, Def is the
// default value, and Value is a pointer to where to store the final
// configured value.
type ConfigParam struct {
    Type  byte
    Mesg  string
    Def   interface{}
    Value interface{}
}

// ConfigMap is a collection of ConfigParam structs, each one named by its
// associated key name.
type ConfigMap map[string]ConfigParam

func cmdlineSetFlags() map[string]bool {
    set := make(map[string]bool)
    flag.Visit(func(f *flag.Flag) {
        set[f.Name] = true
    })

    return set
}

func envName(env_prefix string, flagname string) string {
    return fmt.Sprintf("%s_%s", env_prefix, strings.Replace(strings.ToUpper(flagname), "-", "_", -1))
}

func attemptEnvSet(env_prefix string, flagname string, flag_conf *ConfigParam) {
    env_name := envName(env_prefix, flagname)
    env_val  := os.Getenv(env_name)
    if env_val == "" {
        return
    }

    switch flag_conf.Type {
    case STRING:
        (*flag_conf.Value.(*string)) = env_val
    case INT:
        if val, err := strconv.ParseInt(env_val, 10, 64); err != nil {
            panic(fmt.Sprintf("could not convert %s = %s to int.", env_name, env_val))
        } else {
            (*flag_conf.Value.(*int)) = int(val)
        }
    case BOOL:
        if val, err := strconv.ParseBool(env_val); err != nil {
            panic(fmt.Sprintf("could not convert %s = %s to bool.", env_name, env_val))
        } else {
            (*flag_conf.Value.(*bool)) = val
        }
    }
}

// Init takes the environmental prefix env_name and the ConfigMap conf and
// initializes all configuration variables.  Configuration parameters are
// settable with environment variables prefix with env_name.  For example, if
// env_name = "FLOWSD", and a ConfigParam named "my-filename", a environment
// variable named FLOWSD_MY_FILENAME would set the parameters.  Alternatively,
// they can be set with command line flags (--my-filename in this example).
func Init(env_name string, conf ConfigMap) {
    for name, conf := range conf {
        switch conf.Type {
        case STRING:
            flag.StringVar(conf.Value.(*string), name, conf.Def.(string), conf.Mesg)
        case INT:
            flag.IntVar(conf.Value.(*int), name, conf.Def.(int), conf.Mesg)
        case BOOL:
            flag.BoolVar(conf.Value.(*bool), name, conf.Def.(bool), conf.Mesg)
        }
    }

    flag.Parse()
    set_flags := cmdlineSetFlags()

    for flag, param := range conf {
        if _, ok := set_flags[flag]; !ok {
            attemptEnvSet(env_name, flag, &param)
        }
    }
}
