package configutil

import (
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strconv"
	"time"

	"github.com/mitchellh/mapstructure"
)

// TODO (slackpad) - Trying out a different pattern here for config handling.
// These classes support the flag.Value interface but work in a manner where
// we can tell if they have been set. This lets us work with an all-pointer
// config structure and merge it in a clean-ish way. If this ends up being a
// good pattern we should pull this out into a reusable library.

// ConfigDecodeHook should be passed to mapstructure in order to decode into
// the *Value objects here.
var ConfigDecodeHook = mapstructure.ComposeDecodeHookFunc(
	BoolToBoolValueFunc(),
	StringToDurationValueFunc(),
	StringToStringValueFunc(),
	Float64ToUintValueFunc(),
)

// BoolValue provides a flag value that's aware if it has been set.
type BoolValue struct {
	v *bool
}

// IsBoolFlag is an optional method of the flag.Value
// interface which marks this value as boolean when
// the return value is true. See flag.Value for details.
func (b *BoolValue) IsBoolFlag() bool {
	return true
}

// Merge will overlay this value if it has been set.
func (b *BoolValue) Merge(onto *bool) {
	if b.v != nil {
		*onto = *(b.v)
	}
}

// Set implements the flag.Value interface.
func (b *BoolValue) Set(v string) error {
	if b.v == nil {
		b.v = new(bool)
	}
	var err error
	*(b.v), err = strconv.ParseBool(v)
	return err
}

// String implements the flag.Value interface.
func (b *BoolValue) String() string {
	var current bool
	if b.v != nil {
		current = *(b.v)
	}
	return fmt.Sprintf("%v", current)
}

// BoolToBoolValueFunc is a mapstructure hook that looks for an incoming bool
// mapped to a BoolValue and does the translation.
func BoolToBoolValueFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{}) (interface{}, error) {
		if f.Kind() != reflect.Bool {
			return data, nil
		}

		val := BoolValue{}
		if t != reflect.TypeOf(val) {
			return data, nil
		}

		val.v = new(bool)
		*(val.v) = data.(bool)
		return val, nil
	}
}

// DurationValue provides a flag value that's aware if it has been set.
type DurationValue struct {
	v *time.Duration
}

// Merge will overlay this value if it has been set.
func (d *DurationValue) Merge(onto *time.Duration) {
	if d.v != nil {
		*onto = *(d.v)
	}
}

// Set implements the flag.Value interface.
func (d *DurationValue) Set(v string) error {
	if d.v == nil {
		d.v = new(time.Duration)
	}
	var err error
	*(d.v), err = time.ParseDuration(v)
	return err
}

// String implements the flag.Value interface.
func (d *DurationValue) String() string {
	var current time.Duration
	if d.v != nil {
		current = *(d.v)
	}
	return current.String()
}

// StringToDurationValueFunc is a mapstructure hook that looks for an incoming
// string mapped to a DurationValue and does the translation.
func StringToDurationValueFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{}) (interface{}, error) {
		if f.Kind() != reflect.String {
			return data, nil
		}

		val := DurationValue{}
		if t != reflect.TypeOf(val) {
			return data, nil
		}
		if err := val.Set(data.(string)); err != nil {
			return nil, err
		}
		return val, nil
	}
}

// StringValue provides a flag value that's aware if it has been set.
type StringValue struct {
	v *string
}

// Merge will overlay this value if it has been set.
func (s *StringValue) Merge(onto *string) {
	if s.v != nil {
		*onto = *(s.v)
	}
}

// Set implements the flag.Value interface.
func (s *StringValue) Set(v string) error {
	if s.v == nil {
		s.v = new(string)
	}
	*(s.v) = v
	return nil
}

// String implements the flag.Value interface.
func (s *StringValue) String() string {
	var current string
	if s.v != nil {
		current = *(s.v)
	}
	return current
}

// StringToStringValueFunc is a mapstructure hook that looks for an incoming
// string mapped to a StringValue and does the translation.
func StringToStringValueFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{}) (interface{}, error) {
		if f.Kind() != reflect.String {
			return data, nil
		}

		val := StringValue{}
		if t != reflect.TypeOf(val) {
			return data, nil
		}
		val.v = new(string)
		*(val.v) = data.(string)
		return val, nil
	}
}

// UintValue provides a flag value that's aware if it has been set.
type UintValue struct {
	v *uint
}

// Merge will overlay this value if it has been set.
func (u *UintValue) Merge(onto *uint) {
	if u.v != nil {
		*onto = *(u.v)
	}
}

// Set implements the flag.Value interface.
func (u *UintValue) Set(v string) error {
	if u.v == nil {
		u.v = new(uint)
	}
	parsed, err := strconv.ParseUint(v, 0, 64)
	*(u.v) = (uint)(parsed)
	return err
}

// String implements the flag.Value interface.
func (u *UintValue) String() string {
	var current uint
	if u.v != nil {
		current = *(u.v)
	}
	return fmt.Sprintf("%v", current)
}

// Float64ToUintValueFunc is a mapstructure hook that looks for an incoming
// float64 mapped to a UintValue and does the translation.
func Float64ToUintValueFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{}) (interface{}, error) {
		if f.Kind() != reflect.Float64 {
			return data, nil
		}

		val := UintValue{}
		if t != reflect.TypeOf(val) {
			return data, nil
		}

		fv := data.(float64)
		if fv < 0 {
			return nil, fmt.Errorf("value cannot be negative")
		}

		// The standard guarantees at least this, and this is fine for
		// values we expect to use in configs vs. being fancy with the
		// machine's size for uint.
		if fv > (1<<32 - 1) {
			return nil, fmt.Errorf("value is too large")
		}

		val.v = new(uint)
		*(val.v) = (uint)(fv)
		return val, nil
	}
}

// VisitFn is a callback that gets a chance to visit each file found during a
// traversal with visit().
type VisitFn func(path string) error

// Visit will call the visitor function on the path if it's a file, or for each
// file in the path if it's a directory. Directories will not be recursed into,
// and files in the directory will be visited in alphabetical order.
func Visit(path string, visitor VisitFn) error {
	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("error reading %q: %v", path, err)
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		return fmt.Errorf("error checking %q: %v", path, err)
	}

	if !fi.IsDir() {
		if err := visitor(path); err != nil {
			return fmt.Errorf("error in %q: %v", path, err)
		}
		return nil
	}

	contents, err := f.Readdir(-1)
	if err != nil {
		return fmt.Errorf("error listing %q: %v", path, err)
	}

	sort.Sort(dirEnts(contents))
	for _, fi := range contents {
		if fi.IsDir() {
			continue
		}

		fullPath := filepath.Join(path, fi.Name())
		if err := visitor(fullPath); err != nil {
			return fmt.Errorf("error in %q: %v", fullPath, err)
		}
	}

	return nil
}

// dirEnts applies sort.Interface to directory entries for sorting by name.
type dirEnts []os.FileInfo

func (d dirEnts) Len() int           { return len(d) }
func (d dirEnts) Less(i, j int) bool { return d[i].Name() < d[j].Name() }
func (d dirEnts) Swap(i, j int)      { d[i], d[j] = d[j], d[i] }
