namespace PhpefRuntimeCore {
    public enum ValueKind {
        UNDEF,
        NULL,
        BOOL,
        INT,
        FLOAT,
        STRING,
        ARRAY,
        OBJECT,
        RESOURCE
    }

    public enum Visibility {
        PUBLIC,
        PROTECTED,
        PRIVATE
    }

    private bool stacktrace_initialized = false;

    public delegate Zval UserFunctionHandler(Zval[] args) throws Error;
    public delegate Zval ErrorSuppressedZvalThunk() throws Error;
    public delegate Zval ArrayFilterCallback(Zval value) throws Error;
    public delegate Zval InstanceMethodHandler(Zval this_object, Zval[] args) throws Error;
    public delegate void ClassInstanceInitializer(Zval this_object) throws Error;

    private int error_suppression_depth = 0;

    public bool is_error_suppression_active() {
        return error_suppression_depth > 0;
    }

    public Zval with_error_suppression(ErrorSuppressedZvalThunk thunk) throws Error {
        error_suppression_depth++;
        try {
            return thunk();
        } finally {
            error_suppression_depth--;
        }
    }

    public class UserFunctionMetadata : Object {
        public string php_name { get; private set; }
        public string normalized_php_name { get; private set; }
        public string handler_symbol { get; private set; }
        public int min_args { get; private set; }
        public int max_args { get; private set; }
        public UserFunctionHandler handler;

        public UserFunctionMetadata(string php_name, UserFunctionHandler handler, string handler_symbol, int min_args, int max_args) {
            this.php_name = php_name;
            this.normalized_php_name = normalize_function_name(php_name);
            this.handler = handler;
            this.handler_symbol = handler_symbol;
            this.min_args = min_args;
            this.max_args = max_args;
        }
    }

    public class ClassMetadata : Object {
        public string php_name { get; private set; }
        public string normalized_php_name { get; private set; }
        public HashTable<string, StaticMethodMetadata> static_methods { get; private set; }
        public HashTable<string, InstanceMethodMetadata> instance_methods { get; private set; }
        public ClassInstanceInitializer? instance_initializer { get; set; default = null; }

        public ClassMetadata(string php_name) {
            this.php_name = php_name;
            this.normalized_php_name = normalize_class_name(php_name);
            this.static_methods = new HashTable<string, StaticMethodMetadata>(str_hash, str_equal);
            this.instance_methods = new HashTable<string, InstanceMethodMetadata>(str_hash, str_equal);
        }
    }

    public class StaticMethodMetadata : Object {
        public string class_name { get; private set; }
        public string method_name { get; private set; }
        public string php_name { get; private set; }
        public string normalized_class_name { get; private set; }
        public string normalized_method_name { get; private set; }
        public string handler_symbol { get; private set; }
        public int min_args { get; private set; }
        public int max_args { get; private set; }
        public Visibility visibility { get; private set; }
        public UserFunctionHandler handler;

        public StaticMethodMetadata(string class_name, string method_name, UserFunctionHandler handler, string handler_symbol, int min_args, int max_args, Visibility visibility = Visibility.PUBLIC) {
            this.class_name = class_name;
            this.method_name = method_name;
            this.php_name = class_name + "::" + method_name;
            this.normalized_class_name = normalize_class_name(class_name);
            this.normalized_method_name = normalize_method_name(method_name);
            this.handler = handler;
            this.handler_symbol = handler_symbol;
            this.min_args = min_args;
            this.max_args = max_args;
            this.visibility = visibility;
        }
    }

    public class InstanceMethodMetadata : Object {
        public string class_name { get; private set; }
        public string method_name { get; private set; }
        public string php_name { get; private set; }
        public string normalized_class_name { get; private set; }
        public string normalized_method_name { get; private set; }
        public string handler_symbol { get; private set; }
        public int min_args { get; private set; }
        public int max_args { get; private set; }
        public Visibility visibility { get; private set; }
        public InstanceMethodHandler handler;

        public InstanceMethodMetadata(string class_name, string method_name, InstanceMethodHandler handler, string handler_symbol, int min_args, int max_args, Visibility visibility = Visibility.PUBLIC) {
            this.class_name = class_name;
            this.method_name = method_name;
            this.php_name = class_name + "::" + method_name;
            this.normalized_class_name = normalize_class_name(class_name);
            this.normalized_method_name = normalize_method_name(method_name);
            this.handler = handler;
            this.handler_symbol = handler_symbol;
            this.min_args = min_args;
            this.max_args = max_args;
            this.visibility = visibility;
        }
    }

    private HashTable<string, UserFunctionMetadata>? function_table = null;
    private HashTable<string, UserFunctionMetadata>? function_metadata_by_handler = null;
    private HashTable<string, ClassMetadata>? class_table = null;
    private HashTable<string, StaticMethodMetadata>? static_method_metadata_by_handler = null;
    private HashTable<string, InstanceMethodMetadata>? instance_method_metadata_by_handler = null;
    private string[]? current_args = null;
    public static void init(string[] args) {
        current_args = args;
        try { current_executable_path = FileUtils.read_link("/proc/self/exe"); } catch (FileError e) { current_executable_path = null; }
        if (stacktrace_initialized) {
            return;
        }

        Stacktrace.register_handlers();
        stacktrace_initialized = true;
    }

    public static void exit(Zval code) {
        Process.exit((int) code.as_int());
    }

    public static Zval array_filter_callback(Zval input, ArrayFilterCallback callback) throws Error {
        ZvalArrayEntry[] filtered = {};
        if (input.kind != ValueKind.ARRAY) {
            return Zval.from_array(filtered);
        }
        foreach (var entry in input.as_array_entries()) {
            if (callback(entry.value).to_bool()) {
                filtered += new ZvalArrayEntry(entry.key, entry.value);
            }
        }
        return Zval.from_array(filtered);
    }

    public static Zval argv(Zval index) {
        int i = 0;
        var text = index.to_php_string();
        if (!int.try_parse(text, out i)) {
            return Zval.from_null();
        }
        if (current_args == null || i < 0 || i >= current_args.length) {
            return Zval.from_null();
        }
        return Zval.from_string(current_args[i]);
    }

    public static Zval argv_all() {
        ZvalArrayEntry[] values = {};
        if (current_args == null) {
            return Zval.from_array(values);
        }
        for (int i = 0; i < current_args.length; i++) {
            values += new ZvalArrayEntry(Zval.from_int(i), Zval.from_string(current_args[i]));
        }
        return Zval.from_array(values);
    }

    private static void ensure_function_registry() {
        if (function_table == null) {
            function_table = new HashTable<string, UserFunctionMetadata>(str_hash, str_equal);
        }
        if (function_metadata_by_handler == null) {
            function_metadata_by_handler = new HashTable<string, UserFunctionMetadata>(str_hash, str_equal);
        }
    }

    private static void ensure_class_registry() {
        if (class_table == null) {
            class_table = new HashTable<string, ClassMetadata>(str_hash, str_equal);
        }
        if (static_method_metadata_by_handler == null) {
            static_method_metadata_by_handler = new HashTable<string, StaticMethodMetadata>(str_hash, str_equal);
        }
        if (instance_method_metadata_by_handler == null) {
            instance_method_metadata_by_handler = new HashTable<string, InstanceMethodMetadata>(str_hash, str_equal);
        }
    }

    private static string normalize_function_name(string function_name) {
        return function_name.down();
    }

    private static string normalize_class_name(string class_name) {
        return class_name.down();
    }

    private static string normalize_method_name(string method_name) {
        return method_name.down();
    }

    private static string visibility_php_name(Visibility visibility) {
        switch (visibility) {
            case Visibility.PRIVATE:
                return "private";
            case Visibility.PROTECTED:
                return "protected";
            default:
                return "public";
        }
    }

    private static bool can_call_static_method(StaticMethodMetadata metadata, string current_class_scope) {
        if (metadata.visibility == Visibility.PUBLIC) {
            return true;
        }
        if (current_class_scope == "") {
            return false;
        }
        return metadata.normalized_class_name == normalize_class_name(current_class_scope);
    }

    private static bool can_call_instance_method(InstanceMethodMetadata metadata, string current_class_scope) {
        if (metadata.visibility == Visibility.PUBLIC) {
            return true;
        }
        if (current_class_scope == "") {
            return false;
        }
        return metadata.normalized_class_name == normalize_class_name(current_class_scope);
    }

    public static void register_function(string php_name, UserFunctionHandler handler, string handler_symbol, int min_args = 0, int max_args = 0) throws Error {
        ensure_function_registry();
        var normalized = normalize_function_name(php_name);
        if (function_table != null && function_table.lookup(normalized) != null) {
            throw_exception("Error", Zval.from_string("Cannot redeclare " + php_name + "()"));
        }

        var metadata = new UserFunctionMetadata(php_name, handler, handler_symbol, min_args, max_args);
        function_table.insert(normalized, metadata);
        insert_user_function_handler_metadata(handler_symbol, metadata);
    }


    private static void insert_user_function_handler_metadata(string handler_symbol, UserFunctionMetadata metadata) {
        ensure_function_registry();
        if (function_metadata_by_handler == null) {
            return;
        }
        function_metadata_by_handler.insert(handler_symbol, metadata);
        foreach (var alias in handler_symbol_stacktrace_aliases(handler_symbol)) {
            function_metadata_by_handler.insert(alias, metadata);
        }
    }

    private static void insert_static_method_handler_metadata(string handler_symbol, StaticMethodMetadata metadata) {
        ensure_class_registry();
        if (static_method_metadata_by_handler == null) {
            return;
        }
        static_method_metadata_by_handler.insert(handler_symbol, metadata);
        foreach (var alias in handler_symbol_stacktrace_aliases(handler_symbol)) {
            static_method_metadata_by_handler.insert(alias, metadata);
        }
    }


    private static void insert_instance_method_handler_metadata(string handler_symbol, InstanceMethodMetadata metadata) {
        ensure_class_registry();
        if (instance_method_metadata_by_handler == null) {
            return;
        }
        instance_method_metadata_by_handler.insert(handler_symbol, metadata);
        foreach (var alias in handler_symbol_stacktrace_aliases(handler_symbol)) {
            instance_method_metadata_by_handler.insert(alias, metadata);
        }
    }

    private static string[] handler_symbol_stacktrace_aliases(string handler_symbol) {
        string[] aliases = {};
        append_handler_symbol_stacktrace_aliases(handler_symbol, ref aliases);
        if (handler_symbol.has_suffix("__handler")) {
            append_handler_symbol_stacktrace_aliases(handler_symbol.substring(0, handler_symbol.length - "__handler".length), ref aliases);
        }
        return aliases;
    }

    private static void append_handler_symbol_stacktrace_aliases(string symbol, ref string[] aliases) {
        if (!symbol.contains(".")) {
            return;
        }
        var native_alias = symbol.replace(".", "_");
        if (native_alias == symbol) {
            return;
        }
        aliases = append_string_alias(aliases, native_alias);
        var lowered_native_alias = native_alias.down();
        if (lowered_native_alias != native_alias) {
            aliases = append_string_alias(aliases, lowered_native_alias);
        }
    }

    private static string[] append_string_alias(string[] aliases, string alias) {
        string[] out = {};
        foreach (var existing in aliases) {
            if (existing == alias) {
                return aliases;
            }
            out += existing;
        }
        out += alias;
        return out;
    }

    private static string normalize_stacktrace_symbol_for_lookup(string raw_symbol) {
        var normalized = raw_symbol;
        foreach (var marker in new string[] { ".isra.", ".constprop.", ".part.", ".cold" }) {
            var marker_pos = normalized.index_of(marker);
            if (marker_pos > 0) {
                normalized = normalized.substring(0, marker_pos);
            }
        }
        return normalized;
    }

    public static bool has_registered_function(string php_name) {
        ensure_function_registry();
        return function_table != null && function_table.lookup(normalize_function_name(php_name)) != null;
    }

    public static UserFunctionMetadata? lookup_registered_function(string php_name) {
        ensure_function_registry();
        return function_table != null ? function_table.lookup(normalize_function_name(php_name)) : null;
    }

    public static UserFunctionMetadata? lookup_registered_function_by_handler(string handler_symbol) {
        ensure_function_registry();
        if (function_metadata_by_handler == null) {
            return null;
        }
        var metadata = function_metadata_by_handler.lookup(handler_symbol);
        if (metadata != null) {
            return metadata;
        }
        var normalized_symbol = normalize_stacktrace_symbol_for_lookup(handler_symbol);
        return normalized_symbol != handler_symbol ? function_metadata_by_handler.lookup(normalized_symbol) : null;
    }

    public static void register_class(string php_name, string original_name = "") throws Error {
        ensure_class_registry();
        var visible_name = original_name != "" ? original_name : php_name;
        var normalized = normalize_class_name(php_name);
        if (class_table != null && class_table.lookup(normalized) != null) {
            throw_exception("Error", Zval.from_string("Cannot declare class " + visible_name + ", because the name is already in use"));
        }

        class_table.insert(normalized, new ClassMetadata(visible_name));
    }

    public static bool has_registered_class(string php_name) {
        ensure_class_registry();
        return class_table != null && class_table.lookup(normalize_class_name(php_name)) != null;
    }

    public static ClassMetadata? lookup_registered_class(string php_name) {
        ensure_class_registry();
        return class_table != null ? class_table.lookup(normalize_class_name(php_name)) : null;
    }

    public static void register_static_method(string class_name, string method_name, UserFunctionHandler handler, string handler_symbol, int min_args = 0, int max_args = 0, Visibility visibility = Visibility.PUBLIC) throws Error {
        ensure_class_registry();
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            throw_exception("Error", Zval.from_string("Cannot register static method " + class_name + "::" + method_name + "() for undeclared class"));
        }

        var normalized_method = normalize_method_name(method_name);
        if (class_metadata.static_methods.lookup(normalized_method) != null) {
            throw_exception("Error", Zval.from_string("Cannot redeclare " + class_metadata.php_name + "::" + method_name + "()"));
        }

        var metadata = new StaticMethodMetadata(class_metadata.php_name, method_name, handler, handler_symbol, min_args, max_args, visibility);
        class_metadata.static_methods.insert(normalized_method, metadata);
        insert_static_method_handler_metadata(handler_symbol, metadata);
    }

    public static bool has_registered_static_method(string class_name, string method_name) {
        return lookup_registered_static_method(class_name, method_name) != null;
    }

    public static StaticMethodMetadata? lookup_registered_static_method(string class_name, string method_name) {
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            return null;
        }
        return class_metadata.static_methods.lookup(normalize_method_name(method_name));
    }

    public static StaticMethodMetadata? lookup_registered_static_method_by_handler(string handler_symbol) {
        ensure_class_registry();
        if (static_method_metadata_by_handler == null) {
            return null;
        }
        var metadata = static_method_metadata_by_handler.lookup(handler_symbol);
        if (metadata != null) {
            return metadata;
        }
        var normalized_symbol = normalize_stacktrace_symbol_for_lookup(handler_symbol);
        return normalized_symbol != handler_symbol ? static_method_metadata_by_handler.lookup(normalized_symbol) : null;
    }


    public static void register_instance_method(string class_name, string method_name, InstanceMethodHandler handler, string handler_symbol, int min_args = 0, int max_args = 0, Visibility visibility = Visibility.PUBLIC) throws Error {
        ensure_class_registry();
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            throw_exception("Error", Zval.from_string("Cannot register instance method " + class_name + "::" + method_name + "() for undeclared class"));
        }

        var normalized_method = normalize_method_name(method_name);
        if (class_metadata.instance_methods.lookup(normalized_method) != null) {
            throw_exception("Error", Zval.from_string("Cannot redeclare " + class_metadata.php_name + "::" + method_name + "()"));
        }

        var metadata = new InstanceMethodMetadata(class_metadata.php_name, method_name, handler, handler_symbol, min_args, max_args, visibility);
        class_metadata.instance_methods.insert(normalized_method, metadata);
        insert_instance_method_handler_metadata(handler_symbol, metadata);
    }

    public static void register_class_instance_initializer(string class_name, ClassInstanceInitializer initializer) throws Error {
        ensure_class_registry();
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            throw_exception("Error", Zval.from_string("Cannot register instance initializer for undeclared class " + class_name));
        }
        class_metadata.instance_initializer = initializer;
    }

    public static bool has_registered_instance_method(string class_name, string method_name) {
        return lookup_registered_instance_method(class_name, method_name) != null;
    }

    public static InstanceMethodMetadata? lookup_registered_instance_method(string class_name, string method_name) {
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            return null;
        }
        return class_metadata.instance_methods.lookup(normalize_method_name(method_name));
    }

    public static InstanceMethodMetadata? lookup_registered_instance_method_by_handler(string handler_symbol) {
        ensure_class_registry();
        if (instance_method_metadata_by_handler == null) {
            return null;
        }
        var metadata = instance_method_metadata_by_handler.lookup(handler_symbol);
        if (metadata != null) {
            return metadata;
        }
        var normalized_symbol = normalize_stacktrace_symbol_for_lookup(handler_symbol);
        return normalized_symbol != handler_symbol ? instance_method_metadata_by_handler.lookup(normalized_symbol) : null;
    }

    public static Zval new_object(string class_name, Zval[] args) throws Error {
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            throw_exception("Error", Zval.from_string("Class \"" + class_name + "\" not found"));
        }
        var object_zval = Zval.from_object(new PhpObject(class_metadata));
        if (class_metadata.instance_initializer != null) {
            class_metadata.instance_initializer(object_zval);
        }
        var constructor = class_metadata.instance_methods.lookup(normalize_method_name("__construct"));
        if (constructor == null) {
            if (args.length != 0) {
                throw_exception("ArgumentCountError", Zval.from_string("Object construction arguments are not supported for class " + class_metadata.php_name));
            }
            return object_zval;
        }
        if (args.length < constructor.min_args || args.length > constructor.max_args) {
            throw_exception("ArgumentCountError", Zval.from_string("Too few or too many arguments to function " + constructor.php_name + "()"));
        }
        constructor.handler(object_zval, args);
        return object_zval;
    }

    public static void set_property(Zval receiver, string name, Zval value) throws Error {
        if (receiver.kind != ValueKind.OBJECT || receiver.as_object() == null) {
            throw_exception("Error", Zval.from_string("Attempt to set property " + name + " on non-object"));
        }
        receiver.as_object().set_php_property(name, value);
    }

    public static Zval get_property(Zval receiver, string name) throws Error {
        if (receiver.kind != ValueKind.OBJECT || receiver.as_object() == null) {
            throw_exception("Error", Zval.from_string("Attempt to read property " + name + " on non-object"));
        }
        var value = receiver.as_object().get_php_property(name);
        return value != null ? value : Zval.from_null();
    }

    public static Zval call_instance_method(Zval receiver, string method_name, Zval[] args, string current_class_scope = "") throws Error {
        if (receiver.kind != ValueKind.OBJECT || receiver.as_object() == null) {
            throw_exception("Error", Zval.from_string("Call to a member function " + method_name + "() on non-object"));
        }

        var object_value = receiver.as_object();
        var class_metadata = object_value.class_metadata;
        var metadata = class_metadata.instance_methods.lookup(normalize_method_name(method_name));
        if (metadata == null) {
            throw_exception("Error", Zval.from_string("Call to undefined method " + class_metadata.php_name + "::" + method_name + "()"));
        }

        if (!can_call_instance_method(metadata, current_class_scope)) {
            var visibility_name = visibility_php_name(metadata.visibility);
            var scope_name = current_class_scope != "" ? current_class_scope : "global scope";
            throw_exception("Error", Zval.from_string("Call to " + visibility_name + " method " + metadata.php_name + "() from " + scope_name));
        }

        if (args.length < metadata.min_args || args.length > metadata.max_args) {
            throw_exception("ArgumentCountError", Zval.from_string("Too few or too many arguments to function " + metadata.php_name + "()"));
        }

        return metadata.handler(receiver, args);
    }

    public static Zval call_static_method(string class_name, string method_name, Zval[] args, string current_class_scope = "") throws Error {
        var class_metadata = lookup_registered_class(class_name);
        if (class_metadata == null) {
            throw_exception("Error", Zval.from_string("Class \"" + class_name + "\" not found"));
        }

        var metadata = lookup_registered_static_method(class_name, method_name);
        if (metadata == null) {
            throw_exception("Error", Zval.from_string("Call to undefined method " + class_metadata.php_name + "::" + method_name + "()"));
        }

        if (!can_call_static_method(metadata, current_class_scope)) {
            var visibility_name = visibility_php_name(metadata.visibility);
            var scope_name = current_class_scope != "" ? current_class_scope : "global scope";
            throw_exception("Error", Zval.from_string("Call to " + visibility_name + " method " + metadata.php_name + "() from " + scope_name));
        }

        if (args.length < metadata.min_args || args.length > metadata.max_args) {
            throw_exception("ArgumentCountError", Zval.from_string("Too few or too many arguments to function " + metadata.php_name + "()"));
        }

        return metadata.handler(args);
    }

    public static Zval argument(Zval[] args, int index) throws Error {
        if (index < 0 || index >= args.length) {
            throw_exception("ArgumentCountError", Zval.from_string("Missing function argument"));
        }
        return args[index];
    }

    public static Zval call_function(string php_name, Zval[] args) throws Error {
        var metadata = lookup_registered_function(php_name);
        if (metadata == null) {
            throw_exception("Error", Zval.from_string("Call to undefined function " + php_name + "()"));
        }

        if (args.length < metadata.min_args || args.length > metadata.max_args) {
            throw_exception("ArgumentCountError", Zval.from_string("Too few or too many arguments to function " + metadata.php_name + "()"));
        }

        return metadata.handler(args);
    }

    public class ZvalArrayEntry : Object {
        public Zval? key { get; private set; }
        public Zval value { get; private set; }

        public ZvalArrayEntry(Zval? key, Zval value) {
            this.key = key;
            this.value = value;
        }
    }

    public class Zval {
        public ValueKind kind { get; private set; }

        private bool bool_value;
        private int64 int_value;
        private double float_value;
        private string? string_value;
        private uint8[] string_bytes = {};
        private PhpObject? object_value;
        private void* resource_value;
        private ZvalArrayEntry[] array_entries = {};
        private int64 array_next_index = 0;

        private Zval(ValueKind kind) {
            this.kind = kind;
        }

        public static Zval undefined() {
            return new Zval(ValueKind.UNDEF);
        }

        public bool is_undefined() {
            return kind == ValueKind.UNDEF;
        }

        public static Zval from_null() {
            return new Zval(ValueKind.NULL);
        }

        public static Zval from_bool(bool value) {
            var z = new Zval(ValueKind.BOOL);
            z.bool_value = value;
            return z;
        }

        public static Zval from_int(int64 value) {
            var z = new Zval(ValueKind.INT);
            z.int_value = value;
            return z;
        }

        public static Zval from_float(double value) {
            var z = new Zval(ValueKind.FLOAT);
            z.float_value = value;
            return z;
        }

        private static uint8[] bytes_from_string(string value) {
            uint8[] bytes = {};
            for (int i = 0; i < value.length; i++) {
                bytes += (uint8) value[i];
            }
            return bytes;
        }

        private static uint8[] copy_bytes(uint8[] value) {
            uint8[] copy = {};
            foreach (var b in value) {
                copy += b;
            }
            return copy;
        }

        private static bool bytes_equal(uint8[] left, uint8[] right) {
            if (left.length != right.length) {
                return false;
            }
            for (int i = 0; i < left.length; i++) {
                if (left[i] != right[i]) {
                    return false;
                }
            }
            return true;
        }

        private static string bytes_to_text(uint8[] value) {
            var builder = new StringBuilder();
            foreach (var b in value) {
                if (b == 0) {
                    break;
                }
                builder.append_c((char) b);
            }
            return builder.str;
        }

        public static Zval from_string(string value) {
            var z = new Zval(ValueKind.STRING);
            z.string_value = value;
            z.string_bytes = bytes_from_string(value);
            return z;
        }

        public static Zval from_string_bytes(uint8[] value) {
            var z = new Zval(ValueKind.STRING);
            z.string_value = null;
            z.string_bytes = copy_bytes(value);
            return z;
        }

        public static Zval from_object(PhpObject value) {
            var z = new Zval(ValueKind.OBJECT);
            z.object_value = value;
            return z;
        }

        public static Zval from_resource_pointer(void* value) {
            var z = new Zval(ValueKind.RESOURCE);
            z.resource_value = value;
            return z;
        }

        public static Zval from_array(ZvalArrayEntry[] entries) {
            var z = new Zval(ValueKind.ARRAY);
            int64 next_index = 0;
            foreach (var entry in entries) {
                Zval key;
                if (entry.key == null) {
                    key = Zval.from_int(next_index);
                    next_index++;
                } else {
                    key = entry.key;
                    int64 key_int = 0;
                    if (array_key_to_auto_index(key, out key_int) && key_int >= next_index) {
                        next_index = key_int + 1;
                    }
                }
                z.array_entries += new ZvalArrayEntry(key, entry.value);
            }
            z.array_next_index = next_index;
            return z;
        }

        public void replace_with(Zval other) {
            kind = other.kind;
            bool_value = other.bool_value;
            int_value = other.int_value;
            float_value = other.float_value;
            string_value = other.string_value;
            string_bytes = copy_bytes(other.string_bytes);
            object_value = other.object_value;
            resource_value = other.resource_value;
            array_entries = other.array_entries;
            array_next_index = other.array_next_index;
        }

        public bool as_bool() {
            return bool_value;
        }

        public bool to_bool() {
            switch (kind) {
                case ValueKind.UNDEF:
                case ValueKind.NULL:
                    return false;
                case ValueKind.BOOL:
                    return bool_value;
                case ValueKind.INT:
                    return int_value != 0;
                case ValueKind.FLOAT:
                    return float_value != 0.0;
                case ValueKind.STRING:
                    return string_bytes.length != 0 && !(string_bytes.length == 1 && string_bytes[0] == '0');
                case ValueKind.ARRAY:
                    return array_entries.length > 0;
                case ValueKind.OBJECT:
                    return object_value != null;
                case ValueKind.RESOURCE:
                    return resource_value != null;
                default:
                    return false;
            }
        }

        public int64 as_int() {
            return int_value;
        }

        public double as_float() {
            return float_value;
        }

        public Zval to_float_zval() {
            double double_result = 0;
            int64 int_result = 0;
            bool is_int_result = true;
            if (!numeric_value(out double_result, out int_result, out is_int_result)) {
                return Zval.from_float(0.0);
            }
            return Zval.from_float(double_result);
        }

        public string? as_string() {
            if (string_value != null) {
                return string_value;
            }
            if (kind == ValueKind.STRING) {
                return bytes_to_text(string_bytes);
            }
            return null;
        }

        public uint8[] to_php_bytes() {
            switch (kind) {
                case ValueKind.NULL:
                    return {};
                case ValueKind.BOOL:
                    return bool_value ? bytes_from_string("1") : new uint8[] {};
                case ValueKind.INT:
                    return bytes_from_string(int_value.to_string());
                case ValueKind.FLOAT:
                    return bytes_from_string(float_value.to_string());
                case ValueKind.STRING:
                    return copy_bytes(string_bytes);
                case ValueKind.OBJECT:
                    return bytes_from_string("(OBJECT unsupported currently)");
                case ValueKind.RESOURCE:
                    return bytes_from_string("Resource");
                default:
                    return {};
            }
        }

        public PhpObject? as_object() {
            return object_value;
        }

        public void* as_resource_pointer() {
            return resource_value;
        }

        public ZvalArrayEntry[] as_array_entries() {
            return array_entries;
        }

        public void array_append(Zval value) {
            if (kind != ValueKind.ARRAY) {
                kind = ValueKind.ARRAY;
                array_entries = {};
                array_next_index = 0;
            }
            var key = Zval.from_int(array_next_index);
            array_next_index++;
            array_entries += new ZvalArrayEntry(key, value);
        }
        public void array_set_key(Zval key, Zval value) {
            if (kind != ValueKind.ARRAY) {
                kind = ValueKind.ARRAY;
                array_entries = {};
                array_next_index = 0;
            }
            for (int i = 0; i < array_entries.length; i++) {
                if (array_keys_equal(array_entries[i].key, key)) {
                    array_entries[i] = new ZvalArrayEntry(key, value);
                    return;
                }
            }
            array_entries += new ZvalArrayEntry(key, value);

            int64 index = 0;
            if (array_key_to_auto_index(key, out index) && index >= array_next_index) {
                array_next_index = index + 1;
            }
        }

        public Zval array_get_key(Zval key) {
            if (kind != ValueKind.ARRAY) {
                return Zval.undefined();
            }
            foreach (var entry in array_entries) {
                if (array_keys_equal(entry.key, key)) {
                    return entry.value;
                }
            }
            return Zval.undefined();
        }

        public void array_unset_key(Zval key) {
            if (kind != ValueKind.ARRAY) {
                return;
            }
            ZvalArrayEntry[] kept = {};
            foreach (var entry in array_entries) {
                if (!array_keys_equal(entry.key, key)) {
                    kept += entry;
                }
            }
            array_entries = kept;
        }

        private static bool array_keys_equal(Zval? left, Zval? right) {
            int64 left_index = 0;
            int64 right_index = 0;
            if (array_key_to_auto_index(left, out left_index) && array_key_to_auto_index(right, out right_index)) {
                return left_index == right_index;
            }
            if (left == null || right == null) {
                return left == right;
            }
            if (left.kind == ValueKind.STRING && right.kind == ValueKind.STRING) {
                return (left.as_string() ?? "") == (right.as_string() ?? "");
            }
            if (left.kind == ValueKind.INT && right.kind == ValueKind.INT) {
                return left.as_int() == right.as_int();
            }
            return false;
        }

        private static bool array_key_to_auto_index(Zval? key, out int64 index) {
            index = 0;
            if (key == null) {
                return false;
            }
            if (key.kind == ValueKind.INT) {
                index = key.as_int();
                return index >= 0;
            }
            if (key.kind == ValueKind.STRING) {
                var text = key.as_string() ?? "";
                if (text == "") {
                    return false;
                }
                int64 parsed = 0;
                if (int64.try_parse(text, out parsed) && parsed.to_string() == text && parsed >= 0) {
                    index = parsed;
                    return true;
                }
            }
            return false;
        }

        public string to_php_string() {
            switch (kind) {
                case ValueKind.NULL:
                    return "";
                case ValueKind.BOOL:
                    return bool_value ? "1" : "";
                case ValueKind.INT:
                    return int_value.to_string();
                case ValueKind.FLOAT:
                    return float_value.to_string();
                case ValueKind.STRING:
                    return as_string() ?? "";
                case ValueKind.OBJECT:
                    return "(OBJECT unsupported currently)";
                default:
                    return "";
            }
        }

        public Zval concat(Zval other) {
            uint8[] left = to_php_bytes();
            uint8[] right = other.to_php_bytes();
            uint8[] joined = {};
            foreach (var b in left) {
                joined += b;
            }
            foreach (var b in right) {
                joined += b;
            }
            return Zval.from_string_bytes(joined);
        }

        private bool numeric_value(out double double_result, out int64 int_result, out bool is_int_result) {
            double_result = 0;
            int_result = 0;
            is_int_result = true;

            switch (kind) {
                case ValueKind.NULL:
                    return true;
                case ValueKind.BOOL:
                    int_result = bool_value ? 1 : 0;
                    double_result = int_result;
                    return true;
                case ValueKind.INT:
                    int_result = int_value;
                    double_result = int_value;
                    return true;
                case ValueKind.FLOAT:
                    double_result = float_value;
                    int_result = (int64) float_value;
                    is_int_result = false;
                    return true;
                case ValueKind.STRING:
                    var s = (as_string() ?? "").strip();
                    if (s.index_of(".") >= 0 || s.index_of("e") >= 0 || s.index_of("E") >= 0) {
                        double_result = double.parse(s);
                        int_result = (int64) double_result;
                        is_int_result = false;
                    } else {
                        int_result = int64.parse(s);
                        double_result = int_result;
                        is_int_result = true;
                    }
                    return true;
                default:
                    return false;
            }
        }

        public Zval subtract(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_int(0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_int(left_int - right_int);
            }
            return Zval.from_float(left_double - right_double);
        }
        public Zval multiply(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_int(0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_int(left_int * right_int);
            }
            return Zval.from_float(left_double * right_double);
        }
        public Zval divide(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_int(0);
            }

            return Zval.from_float(left_double / right_double);
        }

        public Zval modulo(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_int(0);
            }
            if (right_int == 0) {
                return Zval.from_int(0);
            }

            return Zval.from_int(left_int % right_int);
        }

        public Zval smaller(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_bool(to_php_string().collate(other.to_php_string()) < 0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_bool(left_int < right_int);
            }
            return Zval.from_bool(left_double < right_double);
        }

        public Zval greater(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_bool(to_php_string().collate(other.to_php_string()) > 0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_bool(left_int > right_int);
            }
            return Zval.from_bool(left_double > right_double);
        }

        public Zval identical(Zval other) {
            if (kind != other.kind) {
                return Zval.from_bool(false);
            }

            switch (kind) {
                case ValueKind.UNDEF:
                case ValueKind.NULL:
                    return Zval.from_bool(true);
                case ValueKind.BOOL:
                    return Zval.from_bool(bool_value == other.bool_value);
                case ValueKind.INT:
                    return Zval.from_bool(int_value == other.int_value);
                case ValueKind.FLOAT:
                    return Zval.from_bool(float_value == other.float_value);
                case ValueKind.STRING:
                    return Zval.from_bool(bytes_equal(string_bytes, other.string_bytes));
                case ValueKind.ARRAY:
                    return Zval.from_bool(this == other);
                case ValueKind.OBJECT:
                    return Zval.from_bool(object_value == other.object_value);
                default:
                    return Zval.from_bool(this == other);
            }
        }

        public Zval equal(Zval other) {
            if (kind == other.kind) {
                return identical(other);
            }

            if (kind == ValueKind.BOOL || other.kind == ValueKind.BOOL) {
                return Zval.from_bool(to_bool() == other.to_bool());
            }

            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (numeric_value(out left_double, out left_int, out left_is_int) &&
                other.numeric_value(out right_double, out right_int, out right_is_int)) {
                if (left_is_int && right_is_int) {
                    return Zval.from_bool(left_int == right_int);
                }
                return Zval.from_bool(left_double == right_double);
            }

            return Zval.from_bool(to_php_string() == other.to_php_string());
        }

        public Zval not_equal(Zval other) {
            return Zval.from_bool(!equal(other).to_bool());
        }

        public Zval not_identical(Zval other) {
            if (kind != other.kind) {
                return Zval.from_bool(true);
            }

            switch (kind) {
                case ValueKind.UNDEF:
                case ValueKind.NULL:
                    return Zval.from_bool(false);
                case ValueKind.BOOL:
                    return Zval.from_bool(bool_value != other.bool_value);
                case ValueKind.INT:
                    return Zval.from_bool(int_value != other.int_value);
                case ValueKind.FLOAT:
                    return Zval.from_bool(float_value != other.float_value);
                case ValueKind.STRING:
                    return Zval.from_bool(!bytes_equal(string_bytes, other.string_bytes));
                case ValueKind.ARRAY:
                    return Zval.from_bool(this != other);
                case ValueKind.OBJECT:
                    return Zval.from_bool(object_value != other.object_value);
                default:
                    return Zval.from_bool(this != other);
            }
        }

        public Zval smaller_or_equal(Zval other) {
            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_bool(to_php_string().collate(other.to_php_string()) <= 0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_bool(left_int <= right_int);
            }
            return Zval.from_bool(left_double <= right_double);
        }

        private static bool array_has_key(ZvalArrayEntry[] entries, Zval? key) {
            foreach (var entry in entries) {
                if (array_keys_equal(entry.key, key)) {
                    return true;
                }
            }
            return false;
        }

        public Zval add(Zval other) {
            if (kind == ValueKind.ARRAY && other.kind == ValueKind.ARRAY) {
                ZvalArrayEntry[] entries = {};
                foreach (var entry in array_entries) {
                    entries += new ZvalArrayEntry(entry.key, entry.value);
                }
                foreach (var entry in other.array_entries) {
                    if (!array_has_key(entries, entry.key)) {
                        entries += new ZvalArrayEntry(entry.key, entry.value);
                    }
                }
                return Zval.from_array(entries);
            }

            double left_double = 0;
            int64 left_int = 0;
            bool left_is_int = true;
            double right_double = 0;
            int64 right_int = 0;
            bool right_is_int = true;

            if (!numeric_value(out left_double, out left_int, out left_is_int) ||
                !other.numeric_value(out right_double, out right_int, out right_is_int)) {
                return Zval.from_int(0);
            }

            if (left_is_int && right_is_int) {
                return Zval.from_int(left_int + right_int);
            }
            return Zval.from_float(left_double + right_double);
        }
    }
}
