/*
Copyright 2020 The Kubernetes 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 reflectutils

import (
	"fmt"
	"strconv"
	"strings"
	"text/scanner"

	"k8s.io/klog/v2"
)

type FieldPath struct {
	elements []FieldPathElement
}

type FieldPathElementType int

const (
	FieldPathElementTypeField = iota
	FieldPathElementTypeMapKey
	FieldPathElementTypeArrayIndex
	FieldPathElementTypeWildcardIndex
)

type FieldPathElement struct {
	Type   FieldPathElementType
	token  string
	number int
}

func (f *FieldPath) Extend(el FieldPathElement) *FieldPath {
	n := len(f.elements)
	newElements := make([]FieldPathElement, n+1)
	copy(newElements, f.elements)
	newElements[n] = el
	return &FieldPath{elements: newElements}
}

func (f *FieldPath) String() string {
	var sb strings.Builder
	for i, el := range f.elements {
		switch el.Type {
		case FieldPathElementTypeField:
			if i != 0 {
				sb.WriteString(".")
			}
			sb.WriteString(el.token)
		case FieldPathElementTypeMapKey:
			sb.WriteString("[")
			sb.WriteString(el.token)
			sb.WriteString("]")
		case FieldPathElementTypeWildcardIndex:
			sb.WriteString("[*]")
		case FieldPathElementTypeArrayIndex:
			sb.WriteString("[")
			sb.WriteString(strconv.Itoa(el.number))
			sb.WriteString("]")
		default:
			klog.Fatalf("unknown token type %+v", el)
		}
	}
	return sb.String()
}

func ParseFieldPath(s string) (*FieldPath, error) {
	var elements []FieldPathElement

	var scan scanner.Scanner

	scan.Init(strings.NewReader(s))
	scan.Mode ^= scanner.SkipComments // don't skip comments

	for tok := scan.Scan(); tok != scanner.EOF; tok = scan.Scan() {
		switch tok {
		case scanner.Ident:
			field := scan.TokenText()
			elements = append(elements, FieldPathElement{
				Type:  FieldPathElementTypeField,
				token: field,
			})

		case '.', '/':
			// Skip

		case '[':
			{
				tok := scan.Scan()
				switch tok {
				case '*':
					elements = append(elements, FieldPathElement{
						Type: FieldPathElementTypeWildcardIndex,
					})

				case scanner.Int:
					v := scan.TokenText()
					n, err := strconv.Atoi(v)
					if err != nil {
						return nil, fmt.Errorf("cannot parse token %q as array-index", v)
					}
					elements = append(elements, FieldPathElement{
						Type:   FieldPathElementTypeArrayIndex,
						number: n,
					})

				default:
					return nil, fmt.Errorf("unexpected token %v (%s)", tok, scan.TokenText())
				}

				tok = scan.Scan()
				switch tok {
				case ']':
					// ok
				default:
					return nil, fmt.Errorf("unexpected token %v (%s)", tok, scan.TokenText())
				}

			}

		default:
			return nil, fmt.Errorf("unexpected token %v (%s)", tok, scan.TokenText())
		}
	}

	return &FieldPath{elements: elements}, nil
}

func (p *FieldPath) IsEmpty() bool {
	return len(p.elements) == 0
}

func (p *FieldPath) Matches(r *FieldPath) bool {
	if len(p.elements) != len(r.elements) {
		return false
	}
	return p.HasPrefixMatch(r)
}

func (p *FieldPath) HasPrefixMatch(r *FieldPath) bool {
	if len(p.elements) < len(r.elements) {
		return false
	}
	for i := range r.elements {
		if p.elements[i] == r.elements[i] {
			continue
		}
		isMatch := false

		matcher := p.elements[i]
		target := r.elements[i]
		switch matcher.Type {
		case FieldPathElementTypeWildcardIndex:
			if target.Type == FieldPathElementTypeArrayIndex {
				isMatch = true
			}
		}
		if !isMatch {
			return false
		}
	}
	return true
}
