package frankenphp

/*
#include "types.h"
*/
import "C"
import (
	"strconv"
	"unsafe"
)

// EXPERIMENTAL: GoString copies a zend_string to a Go string.
func GoString(s unsafe.Pointer) string {
	if s == nil {
		return ""
	}

	zendStr := (*C.zend_string)(s)

	return C.GoStringN((*C.char)(unsafe.Pointer(&zendStr.val)), C.int(zendStr.len))
}

// EXPERIMENTAL: PHPString converts a Go string to a zend_string with copy. The string can be
// non-persistent (automatically freed after the request by the ZMM) or persistent. If you choose
// the second mode, it is your repsonsability to free the allocated memory.
func PHPString(s string, persistent bool) unsafe.Pointer {
	if s == "" {
		return nil
	}

	zendStr := C.zend_string_init(
		(*C.char)(unsafe.Pointer(unsafe.StringData(s))),
		C.size_t(len(s)),
		C._Bool(persistent),
	)

	return unsafe.Pointer(zendStr)
}

// AssociativeArray represents a PHP array with ordered key-value pairs
type AssociativeArray struct {
	Map   map[string]any
	Order []string
}

// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray
func GoAssociativeArray(arr unsafe.Pointer) AssociativeArray {
	entries, order := goArray(arr, true)
	return AssociativeArray{entries, order}
}

// EXPERIMENTAL: GoMap converts a zend_array to an unordered Go map
func GoMap(arr unsafe.Pointer) map[string]any {
	entries, _ := goArray(arr, false)
	return entries
}

func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) {
	if arr == nil {
		panic("received a nil pointer on array conversion")
	}

	zval := (*C.zval)(arr)
	hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY))

	if hashTable == nil {
		panic("received a *zval that wasn't a HashTable on array conversion")
	}

	nNumUsed := hashTable.nNumUsed
	entries := make(map[string]any)
	var order []string
	if ordered {
		order = make([]string, 0, nNumUsed)
	}

	if htIsPacked(hashTable) {
		// if the HashTable is packed, convert all integer keys to strings
		// this is probably a bug by the dev using this function
		// still, we'll (inefficiently) convert to an associative array
		for i := C.uint32_t(0); i < nNumUsed; i++ {
			v := C.get_ht_packed_data(hashTable, i)
			if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
				strIndex := strconv.Itoa(int(i))
				entries[strIndex] = convertZvalToGo(v)
				if ordered {
					order = append(order, strIndex)
				}
			}
		}

		return entries, order
	}

	for i := C.uint32_t(0); i < nNumUsed; i++ {
		bucket := C.get_ht_bucket_data(hashTable, i)
		if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF {
			continue
		}

		v := convertZvalToGo(&bucket.val)

		if bucket.key != nil {
			keyStr := GoString(unsafe.Pointer(bucket.key))
			entries[keyStr] = v
			if ordered {
				order = append(order, keyStr)
			}

			continue
		}

		// as fallback convert the bucket index to a string key
		strIndex := strconv.Itoa(int(bucket.h))
		entries[strIndex] = v
		if ordered {
			order = append(order, strIndex)
		}
	}

	return entries, order
}

// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice
func GoPackedArray(arr unsafe.Pointer) []any {
	if arr == nil {
		panic("GoPackedArray received a nil pointer")
	}

	zval := (*C.zval)(arr)
	hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY))

	if hashTable == nil {
		panic("GoPackedArray received *zval that wasn't a HashTable")
	}

	nNumUsed := hashTable.nNumUsed
	result := make([]any, 0, nNumUsed)

	if htIsPacked(hashTable) {
		for i := C.uint32_t(0); i < nNumUsed; i++ {
			v := C.get_ht_packed_data(hashTable, i)
			if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
				result = append(result, convertZvalToGo(v))
			}
		}

		return result
	}

	// fallback if ht isn't packed - equivalent to array_values()
	for i := C.uint32_t(0); i < nNumUsed; i++ {
		bucket := C.get_ht_bucket_data(hashTable, i)
		if bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF {
			result = append(result, convertZvalToGo(&bucket.val))
		}
	}

	return result
}

// EXPERIMENTAL: PHPMap converts an unordered Go map to a PHP zend_array.
func PHPMap(arr map[string]any) unsafe.Pointer {
	return phpArray(arr, nil)
}

// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array.
func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer {
	return phpArray(arr.Map, arr.Order)
}

func phpArray(entries map[string]any, order []string) unsafe.Pointer {
	var zendArray *C.HashTable

	if len(order) != 0 {
		zendArray = createNewArray((uint32)(len(order)))
		for _, key := range order {
			val := entries[key]
			zval := convertGoToZval(val)
			C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
		}
	} else {
		zendArray = createNewArray((uint32)(len(entries)))
		for key, val := range entries {
			zval := convertGoToZval(val)
			C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
		}
	}

	var zval C.zval
	C.__zval_arr__(&zval, zendArray)

	return unsafe.Pointer(&zval)
}

// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zend_array.
func PHPPackedArray(slice []any) unsafe.Pointer {
	zendArray := createNewArray((uint32)(len(slice)))
	for _, val := range slice {
		zval := convertGoToZval(val)
		C.zend_hash_next_index_insert(zendArray, zval)
	}

	var zval C.zval
	C.__zval_arr__(&zval, zendArray)

	return unsafe.Pointer(&zval)
}

// convertZvalToGo converts a PHP zval to a Go any
func convertZvalToGo(zval *C.zval) any {
	t := C.zval_get_type(zval)
	switch t {
	case C.IS_NULL:
		return nil
	case C.IS_FALSE:
		return false
	case C.IS_TRUE:
		return true
	case C.IS_LONG:
		longPtr := (*C.zend_long)(castZval(zval, C.IS_LONG))
		if longPtr != nil {
			return int64(*longPtr)
		}
		return int64(0)
	case C.IS_DOUBLE:
		doublePtr := (*C.double)(castZval(zval, C.IS_DOUBLE))
		if doublePtr != nil {
			return float64(*doublePtr)
		}
		return float64(0)
	case C.IS_STRING:
		str := (*C.zend_string)(castZval(zval, C.IS_STRING))
		if str == nil {
			return ""
		}

		return GoString(unsafe.Pointer(str))
	case C.IS_ARRAY:
		hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY))
		if hashTable != nil && htIsPacked(hashTable) {
			return GoPackedArray(unsafe.Pointer(zval))
		}

		return GoAssociativeArray(unsafe.Pointer(zval))
	default:
		return nil
	}
}

// convertGoToZval converts a Go any to a PHP zval
func convertGoToZval(value any) *C.zval {
	var zval C.zval

	switch v := value.(type) {
	case nil:
		C.__zval_null__(&zval)
	case bool:
		C.__zval_bool__(&zval, C._Bool(v))
	case int:
		C.__zval_long__(&zval, C.zend_long(v))
	case int64:
		C.__zval_long__(&zval, C.zend_long(v))
	case float64:
		C.__zval_double__(&zval, C.double(v))
	case string:
		str := (*C.zend_string)(PHPString(v, false))
		C.__zval_string__(&zval, str)
	case AssociativeArray:
		return (*C.zval)(PHPAssociativeArray(v))
	case map[string]any:
		return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v}))
	case []any:
		return (*C.zval)(PHPPackedArray(v))
	default:
		C.__zval_null__(&zval)
	}

	return &zval
}

// createNewArray creates a new zend_array with the specified size.
func createNewArray(size uint32) *C.HashTable {
	arr := C.__zend_new_array__(C.uint32_t(size))
	return (*C.HashTable)(unsafe.Pointer(arr))
}

// htIsPacked checks if a HashTable is a list (packed) or hashmap (not packed).
func htIsPacked(ht *C.HashTable) bool {
	flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0]))

	return (flags & C.HASH_FLAG_PACKED) != 0
}

// castZval casts a zval to the expected type and returns a pointer to the value
func castZval(zval *C.zval, expectedType C.uint8_t) unsafe.Pointer {
	if zval == nil || C.zval_get_type(zval) != expectedType {
		return nil
	}

	v := unsafe.Pointer(&zval.value[0])

	switch expectedType {
	case C.IS_LONG:
		return v
	case C.IS_DOUBLE:
		return v
	case C.IS_STRING:
		return unsafe.Pointer(*(**C.zend_string)(v))
	case C.IS_ARRAY:
		return unsafe.Pointer(*(**C.zend_array)(v))
	default:
		return nil
	}
}
