package form

import (
	"reflect"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
)

type cacheFields []cachedField

func (s cacheFields) Len() int {
	return len(s)
}

func (s cacheFields) Less(i, j int) bool {
	return !s[i].isAnonymous
}

func (s cacheFields) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

type cachedField struct {
	idx         int
	name        string
	isAnonymous bool
	isOmitEmpty bool
}

type cachedStruct struct {
	fields cacheFields
}

type structCacheMap struct {
	m     atomic.Value // map[reflect.Type]*cachedStruct
	lock  sync.Mutex
	tagFn TagNameFunc
}

// TagNameFunc allows for adding of a custom tag name parser
type TagNameFunc func(field reflect.StructField) string

func newStructCacheMap() *structCacheMap {

	sc := new(structCacheMap)
	sc.m.Store(make(map[reflect.Type]*cachedStruct))

	return sc
}

func (s *structCacheMap) Get(key reflect.Type) (value *cachedStruct, ok bool) {
	value, ok = s.m.Load().(map[reflect.Type]*cachedStruct)[key]
	return
}

func (s *structCacheMap) Set(key reflect.Type, value *cachedStruct) {

	m := s.m.Load().(map[reflect.Type]*cachedStruct)

	nm := make(map[reflect.Type]*cachedStruct, len(m)+1)
	for k, v := range m {
		nm[k] = v
	}
	nm[key] = value
	s.m.Store(nm)
}

func (s *structCacheMap) parseStruct(mode Mode, current reflect.Value, key reflect.Type, tagName string) *cachedStruct {

	s.lock.Lock()

	// could have been multiple trying to access, but once first is done this ensures struct
	// isn't parsed again.
	cs, ok := s.Get(key)
	if ok {
		s.lock.Unlock()
		return cs
	}

	typ := current.Type()
	cs = &cachedStruct{fields: make([]cachedField, 0, 4)} // init 4, betting most structs decoding into have at aleast 4 fields.

	numFields := typ.NumField()

	var fld reflect.StructField
	var name string
	var idx int
	var isOmitEmpty bool

	for i := 0; i < numFields; i++ {
		isOmitEmpty = false
		fld = typ.Field(i)

		if fld.PkgPath != blank && !fld.Anonymous {
			continue
		}

		if s.tagFn != nil {
			name = s.tagFn(fld)
		} else {
			name = fld.Tag.Get(tagName)
		}

		if name == ignore {
			continue
		}

		if mode == ModeExplicit && len(name) == 0 {
			continue
		}

		// check for omitempty
		if idx = strings.LastIndexByte(name, ','); idx != -1 {
			isOmitEmpty = name[idx+1:] == "omitempty"
			name = name[:idx]
		}

		if len(name) == 0 {
			name = fld.Name
		}

		cs.fields = append(cs.fields, cachedField{idx: i, name: name, isAnonymous: fld.Anonymous, isOmitEmpty: isOmitEmpty})
	}

	sort.Sort(cs.fields)
	s.Set(typ, cs)

	s.lock.Unlock()

	return cs
}
