// Copyright Project Harbor Authors
//
// 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 orm

import (
	"context"
	"reflect"
	"strings"
	"sync"
	"unicode"

	"github.com/astaxie/beego/orm"
	"github.com/goharbor/harbor/src/lib/q"
)

var (
	// cache the parsed models
	cache = sync.Map{}
)

type key struct {
	Name       string
	Filterable bool
	FilterFunc func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter
	Sortable   bool
}

type metadata struct {
	Keys         map[string]*key
	DefaultSorts []*q.Sort
}

func (m *metadata) Filterable(key string) (*key, bool) {
	k, exist := m.Keys[key]
	return k, exist
}

func (m *metadata) Sortable(key string) bool {
	k, exist := m.Keys[key]
	if !exist {
		return false
	}
	return k.Sortable
}

// parse the definition of the provided model(fields/methods/annotations) and return the parsed metadata
func parseModel(model interface{}) *metadata {
	// pointer type
	ptr := reflect.TypeOf(model)
	// struct type
	t := ptr.Elem()

	// get the metadata from cache first
	fullName := getFullName(t)
	cacheMetadata, exist := cache.Load(fullName)
	if exist {
		return cacheMetadata.(*metadata)
	}

	// pointer value
	v := reflect.ValueOf(model)
	metadata := &metadata{
		Keys: map[string]*key{},
	}
	// parse fields of the provided model
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		orm := field.Tag.Get("orm")
		// isn't the database column, skip
		if orm == "-" {
			continue
		}

		filterable := parseFilterable(field)
		defaultSort, sortable := parseSortable(field)
		column := parseColumn(field)

		metadata.Keys[field.Name] = &key{
			Name:       field.Name,
			Filterable: filterable,
			Sortable:   sortable,
		}
		metadata.Keys[column] = &key{
			Name:       column,
			Filterable: filterable,
			Sortable:   sortable,
		}
		if defaultSort != nil {
			metadata.DefaultSorts = []*q.Sort{defaultSort}
		}
	}

	// parse filter methods of the provided model
	for i := 0; i < ptr.NumMethod(); i++ {
		methodName := ptr.Method(i).Name
		if !strings.HasPrefix(methodName, "FilterBy") {
			continue
		}
		methodValue := v.MethodByName(methodName)
		if !methodValue.IsValid() {
			continue
		}
		filterFunc, ok := methodValue.Interface().(func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter)
		if !ok {
			continue
		}
		field := strings.TrimPrefix(methodName, "FilterBy")
		metadata.Keys[field] = &key{
			Name:       field,
			Filterable: true,
			FilterFunc: filterFunc,
		}
		snakeCaseField := snakeCase(field)
		metadata.Keys[snakeCaseField] = &key{
			Name:       snakeCaseField,
			Filterable: true,
			FilterFunc: filterFunc,
		}
	}

	// parse default sorts method
	methodValue := v.MethodByName("GetDefaultSorts")
	if methodValue.IsValid() {
		values := methodValue.Call(nil)
		if len(values) == 1 {
			if sorts, ok := values[0].Interface().([]*q.Sort); ok && len(sorts) > 0 {
				metadata.DefaultSorts = sorts
			}
		}
	}

	cache.Store(fullName, metadata)
	return metadata
}

// parseFilterable parses whether the field is filterable according to the field annotation
// For the following struct definition, "Field1" isn't filterable and "Field2" is filterable
// type Model struct {
//	 Field1 string `filter:"false"`
//	 Field2 string
// }
func parseFilterable(field reflect.StructField) bool {
	return field.Tag.Get("filter") != "false"
}

// parseSortable parses whether the field is sortable according to the field annotation
// If the field is sortable and is also specified as the default sort, return a q.Sort model as well
// For the following struct definition, "Field1" isn't sortable and "Field2", "Field2", "Field4", "Field5" are all sortable
// type Model struct {
//	 Field1 string `sort:"false"`
//	 Field2 string `sort:"true;default"`
//	 Field3 string `sort:"true;default:desc"`
//	 Field4 string `sort:"default"`
//   Field5 string
// }
func parseSortable(field reflect.StructField) (*q.Sort, bool) {
	var defaultSort *q.Sort
	for _, item := range strings.Split(field.Tag.Get("sort"), ";") {
		// isn't sortable, return directly
		if item == "false" {
			return nil, false
		}
		if !strings.HasPrefix(item, "default") {
			continue
		}
		defaultSort = &q.Sort{
			Key:  field.Name,
			DESC: false,
		}
		if strings.TrimPrefix(item, "default") == ":desc" {
			defaultSort.DESC = true
		}
	}
	return defaultSort, true
}

// parseColumn parses the column name according to the field annotation
// type Model struct {
//	 Field1 string `orm:"column(customized_field1)"`
//	 Field2 string
// }
// It returns "customized_field1" for "Field1" and returns "field2" for "Field2"
func parseColumn(field reflect.StructField) string {
	column := ""
	for _, item := range strings.Split(field.Tag.Get("orm"), ";") {
		if !strings.HasPrefix(item, "column") {
			continue
		}
		item = strings.TrimPrefix(item, "column(")
		item = strings.TrimSuffix(item, ")")
		column = item
		break
	}
	if len(column) == 0 {
		column = snakeCase(field.Name)
	}
	return column
}

// convert string to snake case
func snakeCase(str string) string {
	delim := '_'

	runes := []rune(str)

	var out []rune
	for i := 0; i < len(runes); i++ {
		if i > 0 &&
			(unicode.IsUpper(runes[i])) &&
			((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
			out = append(out, delim)
		}
		out = append(out, unicode.ToLower(runes[i]))
	}

	return string(out)
}
