package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"reflect"
	"regexp"
	"strconv"
)

//-------------------------------------------------------------------------
// config
//
// Structure represents persistent config storage of the gocode daemon. Usually
// the config is located somewhere in ~/.config/gocode directory.
//-------------------------------------------------------------------------

type config struct {
	ProposeBuiltins    bool   `json:"propose-builtins"`
	LibPath            string `json:"lib-path"`
	CustomPkgPrefix    string `json:"custom-pkg-prefix"`
	CustomVendorDir    string `json:"custom-vendor-dir"`
	Autobuild          bool   `json:"autobuild"`
	ForceDebugOutput   string `json:"force-debug-output"`
	PackageLookupMode  string `json:"package-lookup-mode"`
	CloseTimeout       int    `json:"close-timeout"`
	UnimportedPackages bool   `json:"unimported-packages"`
	Partials           bool   `json:"partials"`
	IgnoreCase         bool   `json:"ignore-case"`
	ClassFiltering     bool   `json:"class-filtering"`
}

var g_config_desc = map[string]string{
	"propose-builtins":    "If set to {true}, gocode will add built-in types, functions and constants to autocompletion proposals.",
	"lib-path":            "A string option. Allows you to add search paths for packages. By default, gocode only searches {$GOPATH/pkg/$GOOS_$GOARCH} and {$GOROOT/pkg/$GOOS_$GOARCH} in terms of previously existed environment variables. Also you can specify multiple paths using ':' (colon) as a separator (on Windows use semicolon ';'). The paths specified by {lib-path} are prepended to the default ones.",
	"custom-pkg-prefix":   "",
	"custom-vendor-dir":   "",
	"autobuild":           "If set to {true}, gocode will try to automatically build out-of-date packages when their source files are modified, in order to obtain the freshest autocomplete results for them. This feature is experimental.",
	"force-debug-output":  "If is not empty, gocode will forcefully redirect the logging into that file. Also forces enabling of the debug mode on the server side.",
	"package-lookup-mode": "If set to {go}, use standard Go package lookup rules. If set to {gb}, use gb-specific lookup rules. See {https://github.com/constabulary/gb} for details.",
	"close-timeout":       "If there have been no completion requests after this number of seconds, the gocode process will terminate. Default is 30 minutes.",
	"unimported-packages": "If set to {true}, gocode will try to import certain known packages automatically for identifiers which cannot be resolved otherwise. Currently only a limited set of standard library packages is supported.",
	"partials":            "If set to {false}, gocode will not filter autocompletion results based on entered prefix before the cursor. Instead it will return all available autocompletion results viable for a given context. Whether this option is set to {true} or {false}, gocode will return a valid prefix length for output formats which support it. Setting this option to a non-default value may result in editor misbehaviour.",
	"ignore-case":         "If set to {true}, gocode will perform case-insensitive matching when doing prefix-based filtering.",
	"class-filtering":     "Enables or disables gocode's feature where it performs class-based filtering if partial input matches corresponding class keyword: const, var, type, func, package.",
}

var g_default_config = config{
	ProposeBuiltins:    false,
	LibPath:            "",
	CustomPkgPrefix:    "",
	Autobuild:          false,
	ForceDebugOutput:   "",
	PackageLookupMode:  "go",
	CloseTimeout:       1800,
	UnimportedPackages: false,
	Partials:           true,
	IgnoreCase:         false,
	ClassFiltering:     true,
}
var g_config = g_default_config

var g_string_to_bool = map[string]bool{
	"t":     true,
	"true":  true,
	"y":     true,
	"yes":   true,
	"on":    true,
	"1":     true,
	"f":     false,
	"false": false,
	"n":     false,
	"no":    false,
	"off":   false,
	"0":     false,
}

func set_value(v reflect.Value, value string) {
	switch t := v; t.Kind() {
	case reflect.Bool:
		v, ok := g_string_to_bool[value]
		if ok {
			t.SetBool(v)
		}
	case reflect.String:
		t.SetString(value)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		v, err := strconv.ParseInt(value, 10, 64)
		if err == nil {
			t.SetInt(v)
		}
	case reflect.Float32, reflect.Float64:
		v, err := strconv.ParseFloat(value, 64)
		if err == nil {
			t.SetFloat(v)
		}
	}
}

func list_value(v reflect.Value, name string, w io.Writer) {
	switch t := v; t.Kind() {
	case reflect.Bool:
		fmt.Fprintf(w, "%s %v\n", name, t.Bool())
	case reflect.String:
		fmt.Fprintf(w, "%s \"%v\"\n", name, t.String())
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		fmt.Fprintf(w, "%s %v\n", name, t.Int())
	case reflect.Float32, reflect.Float64:
		fmt.Fprintf(w, "%s %v\n", name, t.Float())
	}
}

func (this *config) list() string {
	str, typ := this.value_and_type()
	buf := bytes.NewBuffer(make([]byte, 0, 256))
	for i := 0; i < str.NumField(); i++ {
		v := str.Field(i)
		name := typ.Field(i).Tag.Get("json")
		list_value(v, name, buf)
	}
	return buf.String()
}

func (this *config) list_option(name string) string {
	str, typ := this.value_and_type()
	buf := bytes.NewBuffer(make([]byte, 0, 256))
	for i := 0; i < str.NumField(); i++ {
		v := str.Field(i)
		nm := typ.Field(i).Tag.Get("json")
		if nm == name {
			list_value(v, name, buf)
		}
	}
	return buf.String()
}

func (this *config) set_option(name, value string) string {
	str, typ := this.value_and_type()
	buf := bytes.NewBuffer(make([]byte, 0, 256))
	for i := 0; i < str.NumField(); i++ {
		v := str.Field(i)
		nm := typ.Field(i).Tag.Get("json")
		if nm == name {
			set_value(v, value)
			list_value(v, name, buf)
		}
	}
	this.write()
	return buf.String()

}

func (this *config) value_and_type() (reflect.Value, reflect.Type) {
	v := reflect.ValueOf(this).Elem()
	return v, v.Type()
}

func (this *config) write() error {
	data, err := json.Marshal(this)
	if err != nil {
		return err
	}

	// make sure config dir exists
	dir := config_dir()
	if !file_exists(dir) {
		os.MkdirAll(dir, 0755)
	}

	f, err := os.Create(config_file())
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = f.Write(data)
	if err != nil {
		return err
	}

	return nil
}

func (this *config) read() error {
	data, err := ioutil.ReadFile(config_file())
	if err != nil {
		return err
	}

	err = json.Unmarshal(data, this)
	if err != nil {
		return err
	}

	return nil
}

func quoted(v interface{}) string {
	switch v.(type) {
	case string:
		return fmt.Sprintf("%q", v)
	case int:
		return fmt.Sprint(v)
	case bool:
		return fmt.Sprint(v)
	default:
		panic("unreachable")
	}
}

var descRE = regexp.MustCompile(`{[^}]+}`)

func preprocess_desc(v string) string {
	return descRE.ReplaceAllStringFunc(v, func(v string) string {
		return color_cyan + v[1:len(v)-1] + color_none
	})
}

func (this *config) options() string {
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "%sConfig file location%s: %s\n", color_white_bold, color_none, config_file())
	dv := reflect.ValueOf(g_default_config)
	v, t := this.value_and_type()
	for i, n := 0, t.NumField(); i < n; i++ {
		f := t.Field(i)
		index := f.Index
		tag := f.Tag.Get("json")
		fmt.Fprintf(&buf, "\n%s%s%s\n", color_yellow_bold, tag, color_none)
		fmt.Fprintf(&buf, "%stype%s: %s\n", color_yellow, color_none, f.Type)
		fmt.Fprintf(&buf, "%svalue%s: %s\n", color_yellow, color_none, quoted(v.FieldByIndex(index).Interface()))
		fmt.Fprintf(&buf, "%sdefault%s: %s\n", color_yellow, color_none, quoted(dv.FieldByIndex(index).Interface()))
		fmt.Fprintf(&buf, "%sdescription%s: %s\n", color_yellow, color_none, preprocess_desc(g_config_desc[tag]))
	}

	return buf.String()
}