package chips

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode/utf8"
)

// InstructionHandler is a function type for handling parsed instructions
type InstructionHandler func(opcode string, parameters []string)

// Parser represents a Guacamole protocol parser
type Parser struct {
	// Current buffer of received data
	buffer string

	// Buffer of all received, complete elements
	elementBuffer []string

	// Character offset within buffer of current element's terminator
	elementEnd int

	// Character offset where parser should start looking for next element
	startIndex int

	// Declared length of current element being parsed (in Unicode codepoints)
	elementCodepoints int

	// Instruction handler callback
	OnInstruction InstructionHandler
}

const (
	// Buffer truncation threshold
	bufferTruncationThreshold = 4096

	// Minimum codepoint that requires surrogate pair in UTF-16
	minCodepointRequiresSurrogate = 0x10000
)

// NewParser creates a new Guacamole protocol parser
func NewParser() *Parser {
	return &Parser{
		buffer:            "",
		elementBuffer:     make([]string, 0),
		elementEnd:        -1,
		startIndex:        0,
		elementCodepoints: 0,
		OnInstruction:     nil,
	}
}

// Receive appends instruction data to the parser's internal buffer
// and executes completed instructions
func (p *Parser) Receive(packet string, isBuffer bool) error {
	if isBuffer {
		p.buffer = packet
	} else {
		// Truncate buffer as necessary
		if p.startIndex > bufferTruncationThreshold && p.elementEnd >= p.startIndex {
			p.buffer = p.buffer[p.startIndex:]
			// Reset parse relative to truncation
			p.elementEnd -= p.startIndex
			p.startIndex = 0
		}

		// Append data to buffer
		if len(p.buffer) > 0 {
			p.buffer += packet
		} else {
			p.buffer = packet
		}
	}

	// Parse while search is within currently received data
	for p.elementEnd < len(p.buffer) {
		// If we are waiting for element data
		if p.elementEnd >= p.startIndex {
			// Check if we have enough codepoints for the element
			codepoints := codePointCount(p.buffer, p.startIndex, p.elementEnd)
			if codepoints < p.elementCodepoints {
				p.elementEnd += p.elementCodepoints - codepoints
				continue
			}

			// Check for surrogate pair at element end
			if p.elementCodepoints > 0 && p.elementEnd > 0 {
				if r, _ := utf8.DecodeLastRuneInString(p.buffer[:p.elementEnd]); r >= minCodepointRequiresSurrogate {
					p.elementEnd++
					continue
				}
			}

			// Extract element and terminator
			element := p.buffer[p.startIndex:p.elementEnd]
			var terminator string
			if p.elementEnd < len(p.buffer) {
				terminator = string(p.buffer[p.elementEnd])
			}

			// Add element to buffer
			p.elementBuffer = append(p.elementBuffer, element)

			// Handle instruction termination
			if terminator == ";" {
				// Get opcode (first element)
				if len(p.elementBuffer) == 0 {
					return errors.New("empty instruction")
				}
				opcode := p.elementBuffer[0]
				parameters := p.elementBuffer[1:]

				// Call instruction handler
				if p.OnInstruction != nil {
					p.OnInstruction(opcode, parameters)
				}

				// Clear elements
				p.elementBuffer = make([]string, 0)

				// Truncate buffer if completely parsed
				if !isBuffer && p.elementEnd+1 == len(p.buffer) {
					p.elementEnd = -1
					p.buffer = ""
				}
			} else if terminator != "," {
				return errors.New("element terminator was not ';' or ','")
			}

			// Start searching after terminator
			p.startIndex = p.elementEnd + 1
		}

		// Search for end of length (period character)
		lengthEnd := strings.Index(p.buffer[p.startIndex:], ".")
		if lengthEnd != -1 {
			lengthEnd += p.startIndex // Adjust for substring offset

			// Parse element length
			lengthStr := p.buffer[p.elementEnd+1 : lengthEnd]
			elementCodepoints, err := strconv.Atoi(lengthStr)
			if err != nil {
				return errors.New("non-numeric character in element length")
			}
			p.elementCodepoints = elementCodepoints

			// Calculate element boundaries
			p.startIndex = lengthEnd + 1
			p.elementEnd = p.startIndex + p.elementCodepoints
		} else {
			// No period found, wait for more data
			p.startIndex = len(p.buffer)
			break
		}
	}

	return nil
}

// codePointCount returns the number of Unicode codepoints in a string slice
func codePointCount(str string, start, end int) int {
	if start < 0 {
		start = 0
	}
	if end > len(str) {
		end = len(str)
	}
	if start >= end {
		return 0
	}

	substr := str[start:end]

	// Count surrogate pairs using regex
	surrogatePairRegex := regexp.MustCompile(`[\x{D800}-\x{DBFF}][\x{DC00}-\x{DFFF}]`)
	surrogatePairs := surrogatePairRegex.FindAllString(substr, -1)

	// Each surrogate pair is 2 characters but represents 1 codepoint
	return utf8.RuneCountInString(substr) - len(surrogatePairs)
}

// ToInstruction converts an array of values to a complete Guacamole instruction
func ToInstruction(elements []interface{}) string {
	if len(elements) == 0 {
		return ""
	}

	toElement := func(value interface{}) string {
		str := fmt.Sprintf("%v", value)
		count := utf8.RuneCountInString(str)
		return fmt.Sprintf("%d.%s", count, str)
	}

	instruction := toElement(elements[0])
	for i := 1; i < len(elements); i++ {
		instruction += "," + toElement(elements[i])
	}

	return instruction + ";"
}

// Helper function to convert []string to []interface{}
func genericSliceToInterfaceSlice[T any](elems []T) []interface{} {
	interfaces := make([]interface{}, len(elems))
	for i, s := range elems {
		interfaces[i] = s
	}
	return interfaces
}
