// Copyright 2021 Chaos Mesh 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 genericwebhook

import (
	"container/list"
	"reflect"

	"k8s.io/apimachinery/pkg/util/validation/field"
)

type fieldCallback func(path *field.Path, obj interface{}, field *reflect.StructField) bool

type FieldWalker struct {
	obj      interface{}
	callback fieldCallback
}

func NewFieldWalker(obj interface{}, callback fieldCallback) *FieldWalker {
	return &FieldWalker{
		obj:      obj,
		callback: callback,
	}
}

type iterateNode struct {
	Val   reflect.Value
	Path  *field.Path
	Field *reflect.StructField
}

func (w *FieldWalker) Walk() {
	objVal := reflect.ValueOf(w.obj)

	items := list.New()
	items.PushBack(iterateNode{
		Val:  objVal,
		Path: nil,
	})

	for {
		if items.Len() == 0 {
			break
		}

		item := items.Front()
		items.Remove(item)

		node := item.Value.(iterateNode)
		// If node is not the root node, then we need to check whether
		// we need to iterate its children.
		if node.Path != nil {
			val := node.Val
			if val.Kind() != reflect.Ptr {
				// If it's not a pointer or a slice, then we need to
				// take the address of it, to be able to modify it.
				val = val.Addr()
			}
			if !w.callback(node.Path, val.Interface(), node.Field) {
				continue
			}
		}

		if node.Val.Kind() == reflect.Ptr && node.Val.IsZero() {
			continue
		}
		objVal = reflect.Indirect(node.Val)
		objType := objVal.Type()
		switch objType.Kind() {
		case reflect.Struct:
			for i := 0; i < objVal.NumField(); i++ {
				field := objType.Field(i)
				fieldVal := objVal.Field(i)

				// The field should be exported
				if fieldVal.CanInterface() {
					items.PushBack(iterateNode{
						Val:   fieldVal,
						Path:  node.Path.Child(field.Name),
						Field: &field,
					})
				}
			}
		case reflect.Slice:
			for i := 0; i < objVal.Len(); i++ {
				items.PushBack(iterateNode{
					Val:   objVal.Index(i),
					Path:  node.Path.Index(i),
					Field: nil,
				})
			}
		}

	}
}
