package jsonpointer

import (
	"fmt"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"k8s.io/utils/strings/slices"
)

// Pointer is a JSON pointer that can be retrieved as either as a RFC6901 string or as a JMESPath formatted string.
type Pointer []string

// unquoted identifiers must only contain these characters.
var unquotedFirstCharRangeTable = &unicode.RangeTable{
	R16: []unicode.Range16{
		{Lo: '(', Hi: '(', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
		{Lo: 'A', Hi: 'Z', Stride: 1},
		{Lo: '_', Hi: '_', Stride: 1},
		{Lo: 'a', Hi: 'z', Stride: 1},
	},
}

// unquoted identifiers can contain any combination of these runes.
var unquotedStringRangeTable = &unicode.RangeTable{
	R16: []unicode.Range16{
		{Lo: ')', Hi: ')', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
		{Lo: '0', Hi: '9', Stride: 1},
		{Lo: 'A', Hi: 'Z', Stride: 1},
		{Lo: '_', Hi: '_', Stride: 1},
		{Lo: 'a', Hi: 'z', Stride: 1},
	},
}

// a quoted identifier can contain any of these characters as is.
var unescapedCharRangeTable = &unicode.RangeTable{
	R16: []unicode.Range16{
		{Lo: 0x20, Hi: 0x21, Stride: 1},
		{Lo: 0x23, Hi: 0x5B, Stride: 1},
		{Lo: 0x5D, Hi: 0x1EFF, Stride: 1},
	},
	R32: []unicode.Range32{
		{Lo: 0x1000, Hi: 0x10FFFF, Stride: 1},
	},
	LatinOffset: 0x1EFF - unicode.MaxLatin1,
}

// some special characters must be escaped to be possible to use inside a quoted identifier.
var escapeCharMap = map[rune]string{
	'"':  `\"`, // quotation mark
	'\\': `\\`, // reverse solidus
	'/':  `\/`, // solidus
	'\b': `\b`, // backspace
	'\f': `\f`, // form feed
	'\n': `\n`, // line feed
	'\r': `\r`, // carriage return
	'\t': `\t`, // tab
}

const initialCapacity = 10 // pointers should start with a non-zero capacity to lower the amount of re-allocations done by append.

// New will return an empty Pointer.
func New() Pointer {
	return make([]string, 0, initialCapacity)
}

// Parse will parse the string as a JSON pointer according to RFC 6901.
func Parse(s string) Pointer {
	pointer := New()

	replacer := strings.NewReplacer("~1", "/", "~0", "~", `\\`, `\`, `\"`, `"`)

	for _, component := range strings.FieldsFunc(s, func(r rune) bool {
		return r == '/'
	}) {
		pointer = append(pointer, replacer.Replace(component))
	}

	return pointer
}

// ParsePath will parse the raw path and return it in the form of a Pointer.
func ParsePath(rawPath string) Pointer {
	// Start with a slice with a non-zero capacity to avoid reallocation for most paths.
	pointer := New()

	// Use a string builder and a flush function to append path components to the slice.
	sb := strings.Builder{}

	flush := func() {
		s := sb.String()
		if s != "" {
			pointer = append(pointer, s)
		}
		sb.Reset()
	}

	var pos, width int
	var escaped, quoted bool

	for i := 0; i <= len(rawPath); i += width {
		var r rune
		r, width = utf8.DecodeRuneInString(rawPath[i:])
		if r == utf8.RuneError && width == 1 {
			break
		}

		switch {
		case escaped: // previous character was a backslash.
			sb.WriteRune(r)
			escaped = !escaped
		case r == '\\': // escape character
			escaped = !escaped
		case r == '"': // quoted strings
			if quoted {
				s, _ := strconv.Unquote(rawPath[pos : i+width])
				sb.WriteString(s)
			}
			quoted = !quoted
		case r == '/' && !quoted:
			flush()
		case r == utf8.RuneError: // end of string
			flush()
			return pointer
		default:
			sb.WriteRune(r)
		}

		pos = i + width
	}

	// This is unreachable but we must return something.
	return pointer
}

// JMESPath will return the Pointer in the form of a JMESPath string.
func (p Pointer) JMESPath() string {
	sb := strings.Builder{}

	for _, component := range p {
		// Components that are valid unsigned integers are treated as indices.
		if _, err := strconv.ParseUint(component, 10, 64); err == nil {
			sb.WriteRune('[')
			sb.WriteString(component)
			sb.WriteRune(']')
			continue
		}

		// Write a dot before we write anything, as long as buffer is not empty.
		if sb.Len() > 0 {
			sb.WriteRune('.')
		}

		// If the component starts with a character that is valid as an initial character for an identifier
		// and the remaining characters are also valid for an unquoted identifier then we can append it to
		// the JMESPath as is.
		if ch, _ := utf8.DecodeRuneInString(component); unicode.Is(unquotedFirstCharRangeTable, ch) &&
			strings.IndexFunc(component, func(r rune) bool {
				return !unicode.Is(unquotedStringRangeTable, r)
			}) == -1 {
			sb.WriteString(component)
			continue
		}

		// The component contains characters that are not allowed for unquoted identifiers, so we need to take some extra
		// steps to ensure that it's a valid, quoted identifier.
		sb.WriteRune('"')
		for _, r := range component {
			// Any character in the range table of allowed runes can be written as is.
			if unicode.Is(unescapedCharRangeTable, r) {
				sb.WriteRune(r)
				continue
			}

			// Convert special characters to their escaped sequence.
			if escaped, ok := escapeCharMap[r]; ok {
				sb.WriteString(escaped)
				continue
			}

			// All other characters must be written as unicode escape sequences ay 16 bits a piece.
			if i := utf8.RuneLen(r); i <= 2 {
				// Rune is 1 or 2 bytes.
				_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
			} else {
				_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
				_, _ = fmt.Fprintf(&sb, "\\u%04x", r>>16)
			}
		}
		sb.WriteRune('"')
	}

	// Return the JMESPath.
	return sb.String()
}

// String will return the pointer as a string (RFC6901).
func (p Pointer) String() string {
	sb := strings.Builder{}

	replacer := strings.NewReplacer("~", "~0", "/", "~1", `\`, `\\`, `"`, `\"`)

	for _, component := range p {
		if sb.Len() > 0 {
			sb.WriteRune('/')
		}

		_, _ = replacer.WriteString(&sb, component)
	}

	// Return the pointer.
	return sb.String()
}

// Append will return a Pointer with the strings appended.
func (p Pointer) Append(s ...string) Pointer {
	return append(p, s...)
}

// Prepend will return a Pointer prefixed with the specified strings.
func (p Pointer) Prepend(s ...string) Pointer {
	return append(s, p...)
}

// AppendPath will parse the string as a JSON pointer and return a new pointer.
func (p Pointer) AppendPath(s string) Pointer {
	return append(p, ParsePath(s)...)
}

// SkipN will return a new Pointer where the first N element are stripped.
func (p Pointer) SkipN(n int) Pointer {
	if n > len(p)-1 {
		return []string{}
	}

	return p[n:]
}

// SkipPast will return a new Pointer where every element upto and including the specified string has been stripped off.
func (p Pointer) SkipPast(s string) Pointer {
	return p[slices.Index(p, s)+1:]
}
