/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 hessian

import (
	"fmt"
	"reflect"
	"strings"
	"sync"
	"unicode"
)

import (
	perrors "github.com/pkg/errors"
)

// invalid consts
const (
	InvalidJavaEnum JavaEnum = -1

	ClassKey = "_class"
)

// struct filed tag of hessian
var tagIdentifier = "hessian"

// SetTagIdentifier for customize struct filed tag of hessian, your can use it like:
//
//	hessian.SetTagIdentifier("json")
//	type MyUser struct {
//		UserFullName      string   `json:"user_full_name"`
//		FamilyPhoneNumber string   // default convert to => familyPhoneNumber
//	}
//	var user MyUser
//	hessian.NewEncoder().Encode(user)
func SetTagIdentifier(s string) { tagIdentifier = s }

// POJO interface
// !!! Pls attention that Every field name should be upper case.
// Otherwise the app may panic.
type POJO interface {
	JavaClassName() string // got a go struct's Java Class package name which should be a POJO class.
}

// POJOEnum enum for POJO
type POJOEnum interface {
	POJO
	String() string
	EnumValue(string) JavaEnum
}

// JavaEnum type
type JavaEnum int32

// JavaEnumClass struct
type JavaEnumClass struct {
	name string
}

type ClassInfo struct {
	javaName      string
	fieldNameList []string
	buffer        []byte // encoded buffer
}

type structInfo struct {
	typ      reflect.Type
	goName   string
	javaName string
	index    int // classInfoList index
	inst     interface{}
}

// POJORegistry pojo registry struct
type POJORegistry struct {
	sync.RWMutex
	classInfoList []*ClassInfo           // {class name, field name list...} list
	j2g           map[string]string      // java class name --> go struct name
	registry      map[string]*structInfo // go class name --> go struct info
}

var (
	pojoRegistry = &POJORegistry{
		j2g:      make(map[string]string),
		registry: make(map[string]*structInfo),
	}
	pojoType     = reflect.TypeOf((*POJO)(nil)).Elem()
	javaEnumType = reflect.TypeOf((*POJOEnum)(nil)).Elem()
)

// initDefBuffer initial the class definition buffer, which can be used repeatedly.
func (c *ClassInfo) initDefBuffer() {
	if len(c.buffer) == 0 {
		c.buffer = encByte(c.buffer, BC_OBJECT_DEF)
		c.buffer = encString(c.buffer, c.javaName)
		c.buffer = encInt32(c.buffer, int32(len(c.fieldNameList)))

		for _, fieldName := range c.fieldNameList {
			c.buffer = encString(c.buffer, fieldName)
		}
	}
}

// struct parsing
func showPOJORegistry() {
	pojoRegistry.Lock()
	for k, v := range pojoRegistry.registry {
		fmt.Println("-->> show Registered types <<----")
		fmt.Println(k, v)
	}
	pojoRegistry.Unlock()
}

// RegisterPOJO Register a POJO instance. The return value is -1 if @o has been registered.
func RegisterPOJO(o POJO) int {
	return RegisterPOJOMapping(o.JavaClassName(), o)
}

// RegisterPOJOMapping Register a POJO instance. The return value is -1 if @o has been registered.
func RegisterPOJOMapping(javaClassName string, o interface{}) int {
	// # definition for an object (compact map)
	// class-def  ::= 'C' string int string*
	pojoRegistry.Lock()
	defer pojoRegistry.Unlock()

	if goName, ok := pojoRegistry.j2g[javaClassName]; ok {
		// TODO print warning message about duplicate registration JavaClass
		return pojoRegistry.registry[goName].index
	}

	// JavaClassName shouldn't equal to goName
	if _, ok := pojoRegistry.registry[javaClassName]; ok {
		return -1
	}

	return registerPOJOTypeMapping(javaClassName, GetGoType(o), obtainValueType(o), o)
}

// registerPOJOTypeMapping Register a POJO instance for given type.
// It's used internally to register special types directly.
func registerPOJOTypeMapping(javaClassName string, goName string, typ reflect.Type, o interface{}) int {
	var (
		bHeader   []byte
		bBody     []byte
		fieldList []string
		sttInfo   structInfo
		clsDef    ClassInfo
	)

	sttInfo.typ = typ
	sttInfo.goName = goName
	sttInfo.javaName = javaClassName
	sttInfo.inst = o
	pojoRegistry.j2g[sttInfo.javaName] = sttInfo.goName
	registerListNameMapping(sttInfo.goName, sttInfo.javaName)

	// prepare fields info of objectDef
	nextStruct := []reflect.Type{sttInfo.typ}
	for len(nextStruct) > 0 {
		current := nextStruct[0]
		if current.Kind() == reflect.Struct {
			for i := 0; i < current.NumField(); i++ {
				// skip unexported anonymous filed
				if current.Field(i).PkgPath != "" {
					continue
				}

				structField := current.Field(i)

				// skip ignored field
				tagVal, hasTag := structField.Tag.Lookup(tagIdentifier)
				if tagVal == `-` {
					continue
				}

				// flat anonymous field
				if structField.Anonymous && structField.Type.Kind() == reflect.Struct {
					nextStruct = append(nextStruct, structField.Type)
					continue
				}

				var fieldName string
				if hasTag {
					fieldName = tagVal
				} else {
					fieldName = lowerCamelCase(structField.Name)
				}

				fieldList = append(fieldList, fieldName)
				bBody = encString(bBody, fieldName)
			}
		}

		nextStruct = nextStruct[1:]
	}

	// prepare header of objectDef
	bHeader = encByte(bHeader, BC_OBJECT_DEF)
	bHeader = encString(bHeader, sttInfo.javaName)

	// write fields length into header of objectDef
	// note: cause fieldList is a dynamic slice, so one must calculate length only after it being prepared already.
	bHeader = encInt32(bHeader, int32(len(fieldList)))

	// prepare classDef
	clsDef = ClassInfo{javaName: sttInfo.javaName, fieldNameList: fieldList}

	// merge header and body of objectDef into buffer of ClassInfo
	clsDef.buffer = append(bHeader, bBody...)

	sttInfo.index = len(pojoRegistry.classInfoList)
	pojoRegistry.classInfoList = append(pojoRegistry.classInfoList, &clsDef)
	pojoRegistry.registry[sttInfo.goName] = &sttInfo

	return sttInfo.index
}

// UnRegisterPOJOs unregister POJO instances. It is easy for test.
func UnRegisterPOJOs(os ...POJO) []int {
	arr := make([]int, len(os))
	for i := range os {
		arr[i] = unRegisterPOJO(os[i])
	}

	return arr
}

func unRegisterPOJO(o POJO) int {
	pojoRegistry.Lock()
	defer pojoRegistry.Unlock()

	goName := GetGoType(o)

	if pojoStructInfo, ok := pojoRegistry.registry[goName]; ok {
		delete(pojoRegistry.j2g, pojoStructInfo.javaName)
		listTypeNameMapper.Delete(pojoStructInfo.goName)
		// remove registry cache.
		delete(pojoRegistry.registry, pojoStructInfo.goName)
		// don't remove registry classInfoList,
		// indexes of registered pojo may be affected.
		return pojoStructInfo.index
	}

	return -1
}

// GetGoType get the raw go type name with package.
func GetGoType(o interface{}) string {
	return combineGoTypeName(reflect.TypeOf(o))
}

func combineGoTypeName(t reflect.Type) string {
	for reflect.Ptr == t.Kind() {
		t = t.Elem()
	}

	if reflect.Slice == t.Kind() {
		goName := t.String()
		sliceArrayPrefixIndex := strings.LastIndex(goName, "]")
		for reflect.Slice == t.Kind() {
			t = t.Elem()
		}
		return goName[:sliceArrayPrefixIndex+1] + combineGoTypeName(t)
	}

	pkgPath := t.PkgPath()
	goName := t.String()
	if pkgPath == "" || strings.HasPrefix(goName, pkgPath) {
		return goName
	}
	return pkgPath + "/" + goName
}

func obtainValueType(o interface{}) reflect.Type {
	v := reflect.ValueOf(o)
	switch v.Kind() {
	case reflect.Struct:
		return v.Type()
	case reflect.Ptr:
		return v.Elem().Type()
	}

	return reflect.TypeOf(o)
}

// RegisterPOJOs register a POJO instance arr @os. The return value is @os's
// mathching index array, in which "-1" means its matching POJO has been registered.
func RegisterPOJOs(os ...POJO) []int {
	arr := make([]int, len(os))
	for i := range os {
		arr[i] = RegisterPOJO(os[i])
	}

	return arr
}

// RegisterJavaEnum Register a value type JavaEnum variable.
func RegisterJavaEnum(o POJOEnum) int {
	var (
		ok bool
		b  []byte
		i  int
		n  int
		f  string
		l  []string
		t  structInfo
		c  ClassInfo
		v  reflect.Value
	)

	pojoRegistry.Lock()
	defer pojoRegistry.Unlock()
	if _, ok = pojoRegistry.registry[o.JavaClassName()]; !ok {
		v = reflect.ValueOf(o)
		switch v.Kind() {
		case reflect.Struct:
			t.typ = v.Type()
		case reflect.Ptr:
			t.typ = v.Elem().Type()
		default:
			t.typ = reflect.TypeOf(o)
		}
		t.goName = GetGoType(o)
		t.javaName = o.JavaClassName()
		t.inst = o
		pojoRegistry.j2g[t.javaName] = t.goName

		b = b[:0]
		b = encByte(b, BC_OBJECT_DEF)
		b = encString(b, t.javaName)
		l = l[:0]
		n = 1
		b = encInt32(b, int32(n))
		f = strings.ToLower("name")
		l = append(l, f)
		b = encString(b, f)

		c = ClassInfo{javaName: t.javaName, fieldNameList: l}
		c.buffer = append(c.buffer, b[:]...)
		t.index = len(pojoRegistry.classInfoList)
		pojoRegistry.classInfoList = append(pojoRegistry.classInfoList, &c)
		pojoRegistry.registry[t.goName] = &t
		i = t.index
	} else {
		i = -1
	}

	return i
}

// check if go struct name @goName has been registered or not.
func checkPOJORegistry(v interface{}) (int, bool) {
	s, ok := loadPOJORegistry(v)
	if !ok {
		return -1, false
	}
	return s.index, true
}

// load struct info if go struct name @goName has been registered or not.
func loadPOJORegistry(v interface{}) (*structInfo, bool) {
	var (
		ok bool
		s  *structInfo
	)
	goName := GetGoType(v)
	pojoRegistry.RLock()
	s, ok = pojoRegistry.registry[goName]
	pojoRegistry.RUnlock()

	return s, ok
}

// @typeName is class's java name
func getStructInfo(javaName string) (*structInfo, bool) {
	pojoRegistry.RLock()
	defer pojoRegistry.RUnlock()

	if g, ok := pojoRegistry.j2g[javaName]; ok {
		s, b := pojoRegistry.registry[g]
		return s, b
	}

	return nil, false
}

func getStructDefByIndex(idx int) (reflect.Type, *ClassInfo, error) {
	var (
		ok      bool
		clsName string
		cls     *ClassInfo
		s       *structInfo
	)

	pojoRegistry.RLock()
	defer pojoRegistry.RUnlock()

	if len(pojoRegistry.classInfoList) <= idx || idx < 0 {
		return nil, cls, perrors.Errorf("illegal class index @idx %d", idx)
	}
	cls = pojoRegistry.classInfoList[idx]
	clsName, ok = pojoRegistry.j2g[cls.javaName]
	if !ok {
		return nil, cls, perrors.Errorf("can not find java type name %s in registry", cls.javaName)
	}
	s, ok = pojoRegistry.registry[clsName]
	if !ok {
		return nil, cls, perrors.Errorf("can not find go type name %s in registry", clsName)
	}

	return s.typ, cls, nil
}

// Create a new instance by its struct name is @goName.
// the return value is nil if @o has been registered.
func createInstance(goName string) interface{} {
	var (
		ok bool
		s  *structInfo
	)

	pojoRegistry.RLock()
	s, ok = pojoRegistry.registry[goName]
	pojoRegistry.RUnlock()
	if !ok {
		return nil
	}

	if s.typ.Kind() == reflect.Map {
		return reflect.MakeMap(s.typ).Interface()
	}

	return reflect.New(s.typ).Interface()
}

func lowerCamelCase(s string) string {
	runes := []rune(s)
	runes[0] = unicode.ToLower(runes[0])
	return string(runes)
}

// buildMapClassDef build ClassInfo from map keys.
func buildMapClassDef(javaName string, m map[string]interface{}) (*ClassInfo, error) {
	if javaName == "" {
		var ok bool
		javaName, ok = m[ClassKey].(string)
		if !ok {
			return nil, perrors.Errorf("no java name to build class info from map: %v", m)
		}
	}

	info := &ClassInfo{javaName: javaName}

	_, existClassKey := m[ClassKey]

	for fieldName := range m {
		if existClassKey && fieldName == ClassKey {
			continue
		}

		info.fieldNameList = append(info.fieldNameList, fieldName)
	}

	info.initDefBuffer()

	return info, nil
}
