package schema

import (
	
	
	
	
	
	
	

	
	
)

type callbackType string

const (
	callbackTypeBeforeCreate callbackType = "BeforeCreate"
	callbackTypeBeforeUpdate callbackType = "BeforeUpdate"
	callbackTypeAfterCreate  callbackType = "AfterCreate"
	callbackTypeAfterUpdate  callbackType = "AfterUpdate"
	callbackTypeBeforeSave   callbackType = "BeforeSave"
	callbackTypeAfterSave    callbackType = "AfterSave"
	callbackTypeBeforeDelete callbackType = "BeforeDelete"
	callbackTypeAfterDelete  callbackType = "AfterDelete"
	callbackTypeAfterFind    callbackType = "AfterFind"
)

// ErrUnsupportedDataType unsupported data type
var ErrUnsupportedDataType = errors.New("unsupported data type")

type Schema struct {
	Name                      string
	ModelType                 reflect.Type
	Table                     string
	PrioritizedPrimaryField   *Field
	DBNames                   []string
	PrimaryFields             []*Field
	PrimaryFieldDBNames       []string
	Fields                    []*Field
	FieldsByName              map[string]*Field
	FieldsByBindName          map[string]*Field // embedded fields is 'Embed.Field'
	FieldsByDBName            map[string]*Field
	FieldsWithDefaultDBValue  []*Field // fields with default value assigned by database
	Relationships             Relationships
	CreateClauses             []clause.Interface
	QueryClauses              []clause.Interface
	UpdateClauses             []clause.Interface
	DeleteClauses             []clause.Interface
	BeforeCreate, AfterCreate bool
	BeforeUpdate, AfterUpdate bool
	BeforeDelete, AfterDelete bool
	BeforeSave, AfterSave     bool
	AfterFind                 bool
	err                       error
	initialized               chan struct{}
	namer                     Namer
	cacheStore                *sync.Map
}

func ( Schema) () string {
	if .ModelType.Name() == "" {
		return fmt.Sprintf("%s(%s)", .Name, .Table)
	}
	return fmt.Sprintf("%s.%s", .ModelType.PkgPath(), .ModelType.Name())
}

func ( Schema) () reflect.Value {
	 := reflect.MakeSlice(reflect.SliceOf(reflect.PtrTo(.ModelType)), 0, 20)
	 := reflect.New(.Type())
	.Elem().Set()
	return 
}

func ( Schema) ( string) *Field {
	if ,  := .FieldsByDBName[];  {
		return 
	}
	if ,  := .FieldsByName[];  {
		return 
	}
	return nil
}

// LookUpFieldByBindName looks for the closest field in the embedded struct.
//
//	type Struct struct {
//		Embedded struct {
//			ID string // is selected by LookUpFieldByBindName([]string{"Embedded", "ID"}, "ID")
//		}
//		ID string // is selected by LookUpFieldByBindName([]string{"ID"}, "ID")
//	}
func ( Schema) ( []string,  string) *Field {
	if len() == 0 {
		return nil
	}
	for  := len() - 1;  >= 0; -- {
		 := strings.Join([:], ".") + "." + 
		if ,  := .FieldsByBindName[];  {
			return 
		}
	}
	return nil
}

type Tabler interface {
	TableName() string
}

type TablerWithNamer interface {
	TableName(Namer) string
}

// Parse get data type from dialector
func ( interface{},  *sync.Map,  Namer) (*Schema, error) {
	return ParseWithSpecialTableName(, , , "")
}

// ParseWithSpecialTableName get data type from dialector with extra schema table
func ( interface{},  *sync.Map,  Namer,  string) (*Schema, error) {
	if  == nil {
		return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, )
	}

	 := reflect.ValueOf()
	if .Kind() == reflect.Ptr && .IsNil() {
		 = reflect.New(.Type().Elem())
	}
	 := reflect.Indirect().Type()

	if .Kind() == reflect.Interface {
		 = reflect.Indirect(reflect.ValueOf()).Elem().Type()
	}

	for .Kind() == reflect.Slice || .Kind() == reflect.Array || .Kind() == reflect.Ptr {
		 = .Elem()
	}

	if .Kind() != reflect.Struct {
		if .PkgPath() == "" {
			return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, )
		}
		return nil, fmt.Errorf("%w: %s.%s", ErrUnsupportedDataType, .PkgPath(), .Name())
	}

	// Cache the Schema for performance,
	// Use the modelType or modelType + schemaTable (if it present) as cache key.
	var  interface{}
	if  != "" {
		 = fmt.Sprintf("%p-%s", , )
	} else {
		 = 
	}

	// Load exist schema cache, return if exists
	if ,  := .Load();  {
		 := .(*Schema)
		// Wait for the initialization of other goroutines to complete
		<-.initialized
		return , .err
	}

	 := reflect.New()
	 := .TableName(.Name())
	if ,  := .Interface().(Tabler);  {
		 = .TableName()
	}
	if ,  := .Interface().(TablerWithNamer);  {
		 = .TableName()
	}
	if ,  := .(embeddedNamer);  {
		 = .Table
	}
	if  != "" &&  !=  {
		 = 
	}

	 := &Schema{
		Name:             .Name(),
		ModelType:        ,
		Table:            ,
		FieldsByName:     map[string]*Field{},
		FieldsByBindName: map[string]*Field{},
		FieldsByDBName:   map[string]*Field{},
		Relationships:    Relationships{Relations: map[string]*Relationship{}},
		cacheStore:       ,
		namer:            ,
		initialized:      make(chan struct{}),
	}
	// When the schema initialization is completed, the channel will be closed
	defer close(.initialized)

	// Load exist schema cache, return if exists
	if ,  := .Load();  {
		 := .(*Schema)
		// Wait for the initialization of other goroutines to complete
		<-.initialized
		return , .err
	}

	for  := 0;  < .NumField(); ++ {
		if  := .Field(); ast.IsExported(.Name) {
			if  := .ParseField(); .EmbeddedSchema != nil {
				.Fields = append(.Fields, .EmbeddedSchema.Fields...)
			} else {
				.Fields = append(.Fields, )
			}
		}
	}

	for ,  := range .Fields {
		if .DBName == "" && .DataType != "" {
			.DBName = .ColumnName(.Table, .Name)
		}

		 := .BindName()
		if .DBName != "" {
			// nonexistence or shortest path or first appear prioritized if has permission
			if ,  := .FieldsByDBName[.DBName]; ! || ((.Creatable || .Updatable || .Readable) && len(.BindNames) < len(.BindNames)) {
				if ,  := .FieldsByDBName[.DBName]; ! {
					.DBNames = append(.DBNames, .DBName)
				}
				.FieldsByDBName[.DBName] = 
				.FieldsByName[.Name] = 
				.FieldsByBindName[] = 

				if  != nil && .PrimaryKey {
					for ,  := range .PrimaryFields {
						if  ==  {
							.PrimaryFields = append(.PrimaryFields[0:], .PrimaryFields[+1:]...)
						}
					}
				}

				if .PrimaryKey {
					.PrimaryFields = append(.PrimaryFields, )
				}
			}
		}

		if ,  := .FieldsByName[.Name]; ! || .TagSettings["-"] == "-" {
			.FieldsByName[.Name] = 
		}
		if ,  := .FieldsByBindName[]; ! || .TagSettings["-"] == "-" {
			.FieldsByBindName[] = 
		}

		.setupValuerAndSetter()
	}

	 := .LookUpField("id")
	if  == nil {
		 = .LookUpField("ID")
	}

	if  != nil {
		if .PrimaryKey {
			.PrioritizedPrimaryField = 
		} else if len(.PrimaryFields) == 0 {
			.PrimaryKey = true
			.PrioritizedPrimaryField = 
			.PrimaryFields = append(.PrimaryFields, )
		}
	}

	if .PrioritizedPrimaryField == nil {
		if len(.PrimaryFields) == 1 {
			.PrioritizedPrimaryField = .PrimaryFields[0]
		} else if len(.PrimaryFields) > 1 {
			// If there are multiple primary keys, the AUTOINCREMENT field is prioritized
			for ,  := range .PrimaryFields {
				if .AutoIncrement {
					.PrioritizedPrimaryField = 
					break
				}
			}
		}
	}

	for ,  := range .PrimaryFields {
		.PrimaryFieldDBNames = append(.PrimaryFieldDBNames, .DBName)
	}

	for ,  := range .Fields {
		if .DataType != "" && .HasDefaultValue && .DefaultValueInterface == nil {
			.FieldsWithDefaultDBValue = append(.FieldsWithDefaultDBValue, )
		}
	}

	if  := .PrioritizedPrimaryField;  != nil {
		switch .GORMDataType {
		case Int, Uint:
			if ,  := .TagSettings["AUTOINCREMENT"]; ! {
				if !.HasDefaultValue || .DefaultValueInterface != nil {
					.FieldsWithDefaultDBValue = append(.FieldsWithDefaultDBValue, )
				}

				.HasDefaultValue = true
				.AutoIncrement = true
			}
		}
	}

	 := []callbackType{
		callbackTypeBeforeCreate, callbackTypeAfterCreate,
		callbackTypeBeforeUpdate, callbackTypeAfterUpdate,
		callbackTypeBeforeSave, callbackTypeAfterSave,
		callbackTypeBeforeDelete, callbackTypeAfterDelete,
		callbackTypeAfterFind,
	}
	for ,  := range  {
		if  := callBackToMethodValue(, ); .IsValid() {
			switch .Type().String() {
			case "func(*gorm.DB) error": // TODO hack
				reflect.Indirect(reflect.ValueOf()).FieldByName(string()).SetBool(true)
			default:
				logger.Default.Warn(context.Background(), "Model %v don't match %vInterface, should be `%v(*gorm.DB) error`. Please see https://gorm.io/docs/hooks.html", , , )
			}
		}
	}

	// Cache the schema
	if ,  := .LoadOrStore(, );  {
		 := .(*Schema)
		// Wait for the initialization of other goroutines to complete
		<-.initialized
		return , .err
	}

	defer func() {
		if .err != nil {
			logger.Default.Error(context.Background(), .err.Error())
			.Delete()
		}
	}()

	if ,  := .cacheStore.Load(embeddedCacheKey); ! {
		for ,  := range .Fields {
			if .DataType == "" && (.Creatable || .Updatable || .Readable) {
				if .parseRelation(); .err != nil {
					return , .err
				} else {
					.FieldsByName[.Name] = 
					.FieldsByBindName[.BindName()] = 
				}
			}

			 := reflect.New(.IndirectFieldType)
			 := .Interface()
			if ,  := .(CreateClausesInterface);  {
				.Schema.CreateClauses = append(.Schema.CreateClauses, .CreateClauses()...)
			}

			if ,  := .(QueryClausesInterface);  {
				.Schema.QueryClauses = append(.Schema.QueryClauses, .QueryClauses()...)
			}

			if ,  := .(UpdateClausesInterface);  {
				.Schema.UpdateClauses = append(.Schema.UpdateClauses, .UpdateClauses()...)
			}

			if ,  := .(DeleteClausesInterface);  {
				.Schema.DeleteClauses = append(.Schema.DeleteClauses, .DeleteClauses()...)
			}
		}
	}

	return , .err
}

// This unrolling is needed to show to the compiler the exact set of methods
// that can be used on the modelType.
// Prior to go1.22 any use of MethodByName would cause the linker to
// abandon dead code elimination for the entire binary.
// As of go1.22 the compiler supports one special case of a string constant
// being passed to MethodByName. For enterprise customers or those building
// large binaries, this gives a significant reduction in binary size.
// https://github.com/golang/go/issues/62257
func callBackToMethodValue( reflect.Value,  callbackType) reflect.Value {
	switch  {
	case callbackTypeBeforeCreate:
		return .MethodByName(string(callbackTypeBeforeCreate))
	case callbackTypeAfterCreate:
		return .MethodByName(string(callbackTypeAfterCreate))
	case callbackTypeBeforeUpdate:
		return .MethodByName(string(callbackTypeBeforeUpdate))
	case callbackTypeAfterUpdate:
		return .MethodByName(string(callbackTypeAfterUpdate))
	case callbackTypeBeforeSave:
		return .MethodByName(string(callbackTypeBeforeSave))
	case callbackTypeAfterSave:
		return .MethodByName(string(callbackTypeAfterSave))
	case callbackTypeBeforeDelete:
		return .MethodByName(string(callbackTypeBeforeDelete))
	case callbackTypeAfterDelete:
		return .MethodByName(string(callbackTypeAfterDelete))
	case callbackTypeAfterFind:
		return .MethodByName(string(callbackTypeAfterFind))
	default:
		return reflect.ValueOf(nil)
	}
}

func getOrParse( interface{},  *sync.Map,  Namer) (*Schema, error) {
	 := reflect.ValueOf().Type()
	for .Kind() == reflect.Slice || .Kind() == reflect.Array || .Kind() == reflect.Ptr {
		 = .Elem()
	}

	if .Kind() != reflect.Struct {
		if .PkgPath() == "" {
			return nil, fmt.Errorf("%w: %+v", ErrUnsupportedDataType, )
		}
		return nil, fmt.Errorf("%w: %s.%s", ErrUnsupportedDataType, .PkgPath(), .Name())
	}

	if ,  := .Load();  {
		return .(*Schema), nil
	}

	return Parse(, , )
}