/*
 * Copyright 2021 ByteDance Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package caching

import (
	"strings"
	"unicode"
	"unsafe"

	"github.com/bytedance/sonic/internal/envs"
	"github.com/bytedance/sonic/internal/native"
	"github.com/bytedance/sonic/internal/resolver"
	"github.com/bytedance/sonic/internal/rt"
)

const _AlignSize =  32
const _PaddingSize =  32

type FieldLookup interface {
	Set(fields []resolver.FieldMeta)
	Get(name string) int
}

func isAscii(s string) bool {
	for i :=0; i < len(s); i++ {
		if s[i] > unicode.MaxASCII {
			return false
		}
	}
	return true
}

func NewFieldLookup(fields []resolver.FieldMeta) FieldLookup {
	var f FieldLookup
	isAsc := true
	n := len(fields)

	// when field name has non-ascii, use the fallback methods to use strings.ToLower
	for _, f := range fields {
		if !isAscii(f.Name) {
			isAsc = false
			break
		}
	}

	if n <= 8 {
		f =  NewSmallFieldMap(n)
	} else if envs.UseFastMap && n <= 128 && isAsc {
		f =   NewNormalFieldMap(n)
	} else {
		f =   NewFallbackFieldMap(n)
	}

	f.Set(fields)
	return f
}

// Map for keys nums max 8, idx is in [0, 8)
type SmallFieldMap struct {
	keys []string
	lowerKeys []string
}

func NewSmallFieldMap (hint int) *SmallFieldMap {
	return &SmallFieldMap{
		keys: make([]string, hint, hint),
		lowerKeys: make([]string, hint, hint),
	}
}

func (self *SmallFieldMap) Set(fields []resolver.FieldMeta) {
	if len(fields) > 8 {
		panic("small field map shoud use in small struct")
	}

	for i, f := range fields {
		self.keys[i] = f.Name
		self.lowerKeys[i] = strings.ToLower(f.Name)
	}
}

func (self *SmallFieldMap) Get(name string) int {
	for i, k := range self.keys {
		if len(k) == len(name) && k == name {
			return i
		}
	}

	name = strings.ToLower(name)
	for i, k := range self.lowerKeys {
		if len(k) == len(name) && k == name {
			return i
		}
	}
	return -1
}


/*
1. select by the length: 0 ~ 32 and larger lengths
2. simd match the aligned prefix of the keys: 4/8/16/32 bytes or larger keys
3. check the key with strict match
4. check the key with case-insensitive match
5. find the index 

Mem Layout:
     fixed 33 * 5 bytes  165 bytes |||  variable keys  ||| variable lowerkeys
| length metadata array[33] ||| key0.0 | u8 | key0.1 | u8 | ...  || key1.0 | u8 | key1.1 | u8 | ...  ||| lowerkeys info ...

*/

// Map for keys nums max 255, idx is in [0, 255), idx 255 means not found.
// keysoffset
// | metadata | aligned key0 | aligned key1 | ... |
// 1 ~ 8
// 8 ~ 16
// 16 ~ 32
// > 32 keys use the long keys entry lists
// use bytes to reduce GC
type NormalFieldMap struct {
	keys  			[]byte
	longKeys		[]keyEntry
	// offset for lower
	lowOffset	    int
}

type keyEntry struct {
	key 		string
	lowerKey	string
	index		uint
}

func NewNormalFieldMap(n int) *NormalFieldMap {
	return &NormalFieldMap{
	}
}

const _HdrSlot = 33
const _HdrSize = _HdrSlot * 5

// use native SIMD to accelerate it
func (self *NormalFieldMap) Get(name string) int {
	// small keys use native C
	if len(name) <= 32 {
		 _ = native.LookupSmallKey
		return native.LookupSmallKey(&name, &self.keys, self.lowOffset);
	}
	return self.getLongKey(name)
}

func (self *NormalFieldMap) getLongKey(name string) int {
	for _, k := range self.longKeys {
		if len(k.key) != len(name) {
			continue;
		}
		if k.key == name {
			return int(k.index)
		}
	}

	lower := strings.ToLower(name)
	for _, k := range self.longKeys {
		if len(k.key) != len(name) {
			continue;
		}

		if k.lowerKey == lower {
			return int(k.index)
		}
	}
	return -1
}

func (self *NormalFieldMap) Getdouble(name string) int {
	if len(name) > 32 {
		for _, k := range self.longKeys {
			if len(k.key) != len(name) {
				continue;
			}
			if k.key == name {
				return int(k.index)
			}
		}
		return self.getCaseInsensitive(name)
	}

	// check the fixed length keys, not found the target length
	cnt := int(self.keys[5 * len(name)])
	if cnt == 0 {
		return -1
	}
	p := ((*rt.GoSlice)(unsafe.Pointer(&self.keys))).Ptr
	offset := int(*(*int32)(unsafe.Pointer(uintptr(p) + uintptr(5 * len(name) + 1)))) + _HdrSize
	for i := 0; i < cnt; i++ {
		key := rt.Mem2Str(self.keys[offset: offset + len(name)])
		if key == name {
			return int(self.keys[offset + len(name)])
		}
		offset += len(name) + 1
	}

	return self.getCaseInsensitive(name)
}

func (self *NormalFieldMap) getCaseInsensitive(name string) int {
	lower := strings.ToLower(name)
	if len(name) > 32 {
		for _, k := range self.longKeys {
			if len(k.key) != len(name) {
				continue;
			}

			if k.lowerKey == lower {
				return int(k.index)
			}
		}
		return -1
	}

	cnt := int(self.keys[5 * len(name)])
	p := ((*rt.GoSlice)(unsafe.Pointer(&self.keys))).Ptr
	offset := int(*(*int32)(unsafe.Pointer(uintptr(p) + uintptr(5 * len(name) + 1)))) + self.lowOffset
	for i := 0; i < cnt; i++ {
		key := rt.Mem2Str(self.keys[offset: offset + len(name)])
		if key == lower {
			return int(self.keys[offset + len(name)])
		}
		offset += len(name) + 1
	}

	return -1
}

type keysInfo struct {
	counts int
	lenSum int
	offset int
	cur    int
}

func (self *NormalFieldMap) Set(fields []resolver.FieldMeta) {
	if len(fields) <=8 || len(fields) > 128 {
		panic("normal field map shoud use in small struct")
	}

	// allocate the flat map in []byte
	var keyLenSum [_HdrSlot]keysInfo

	for i := 0; i < _HdrSlot; i++ {
		keyLenSum[i].offset = 0
		keyLenSum[i].counts = 0
		keyLenSum[i].lenSum = 0
		keyLenSum[i].cur = 0
	}

	kvLen := 0
	for _, f := range(fields) {
		len := len(f.Name)
		if len <= 32 {
			kvLen += len + 1 // key + index
			keyLenSum[len].counts++
			keyLenSum[len].lenSum += len + 1
		}

	}

	// add a padding size at last to make it firendly for SIMD.
	self.keys = make([]byte, _HdrSize + 2 * kvLen, _HdrSize + 2 * kvLen + _PaddingSize)
	self.lowOffset = _HdrSize + kvLen

	// initialize all keys offset
	self.keys[0] = byte(keyLenSum[0].counts)
	// self.keys[1:5] = 0 // offset is always zero here.
	i := 1
	p := ((*rt.GoSlice)(unsafe.Pointer(&self.keys))).Ptr
	for i < _HdrSlot {
		keyLenSum[i].offset = keyLenSum[i-1].offset + keyLenSum[i-1].lenSum
		self.keys[i * 5] = byte(keyLenSum[i].counts)
		// write the offset into []byte
		*(*int32)(unsafe.Pointer(uintptr(p) + uintptr(i * 5 + 1))) = int32(keyLenSum[i].offset)
		i += 1

	}

	// fill the key into bytes
	for i, f := range(fields) {
		len := len(f.Name)
		if len <= 32 {
			offset := keyLenSum[len].offset +  keyLenSum[len].cur
			copy(self.keys[_HdrSize + offset: ], f.Name)
			copy(self.keys[self.lowOffset + offset: ], strings.ToLower(f.Name))
			self.keys[_HdrSize + offset + len] = byte(i)
			self.keys[self.lowOffset + offset + len] = byte(i)
			keyLenSum[len].cur += len + 1

		} else {
			self.longKeys = append(self.longKeys, keyEntry{f.Name, strings.ToLower(f.Name), uint(i)})
		}
	}

}

// use hashnap
type FallbackFieldMap struct {
	oders  []string
	inner  map[string]int
	backup map[string]int
}
 
 func NewFallbackFieldMap(n int) *FallbackFieldMap {
	 return &FallbackFieldMap{
		 oders:  make([]string, n, n),
		 inner:  make(map[string]int, n*2),
		 backup: make(map[string]int, n*2),
	 }
 }
 
 func (self *FallbackFieldMap) Get(name string) int {
	 if i, ok := self.inner[name]; ok {
		 return i
	 } else {
		 return self.getCaseInsensitive(name)
	 }
 }
 
 func (self *FallbackFieldMap) Set(fields []resolver.FieldMeta) {

	for i, f := range(fields) {
		name := f.Name
		self.oders[i] = name
		self.inner[name] = i
	
		/* add the case-insensitive version, prefer the one with smaller field ID */
		key := strings.ToLower(name)
		if v, ok := self.backup[key]; !ok || i < v {
			self.backup[key] = i
		}
	}
 }
 
 func (self *FallbackFieldMap) getCaseInsensitive(name string) int {
	 if i, ok := self.backup[strings.ToLower(name)]; ok {
		 return i
	 } else {
		 return -1
	 }
 }
 