namespace PhpefRuntimeCore {
    private const string THROWABLE_REF_PREFIX = "PHPEF_THROWABLE_REF\t";
    private int64 next_throwable_token = 1;
    private HashTable<string, Zval>? throwable_payloads = null;

    private static void ensure_throwable_payload_registry() {
        if (throwable_payloads == null) {
            throwable_payloads = new HashTable<string, Zval>(str_hash, str_equal);
        }
    }

    private static string normalize_php_exception_class(string class_name) {
        var normalized = class_name;
        while (normalized.has_prefix("\\")) {
            normalized = normalized.substring(1);
        }
        return normalized == "" ? "Exception" : normalized;
    }

    private static string throwable_token_from_error(Error e) {
        var message = e.message ?? "";
        var first_line = message.split("\n", 2)[0];
        if (!first_line.has_prefix(THROWABLE_REF_PREFIX)) {
            return "";
        }
        return first_line.substring(THROWABLE_REF_PREFIX.length);
    }

    private static PhpThrowableObject? throwable_object_from_zval(Zval value) {
        if (value.kind != ValueKind.OBJECT) {
            return null;
        }
        return value.as_object() as PhpThrowableObject;
    }

    public class PhpObject : Object {
        public ClassMetadata class_metadata { get; private set; }
        private HashTable<string, Zval> properties;

        public PhpObject(ClassMetadata class_metadata) {
            this.class_metadata = class_metadata;
            this.properties = new HashTable<string, Zval>(str_hash, str_equal);
        }

        public void set_php_property(string name, Zval value) {
            properties.replace(name, value);
        }

        public Zval? get_php_property(string name) {
            return properties.lookup(name);
        }
    }

    public class PhpThrowableObject : PhpObject {
        public Zval php_message { get; private set; }
        public Zval php_code { get; private set; }
        public string file_name { get; private set; }
        public int line { get; private set; }
        public Zval? previous { get; private set; }
        public Zval php_trace { get; private set; }

        public PhpThrowableObject(string class_name, Zval message, Zval code, string file_name = "", int line = 0, Zval? previous = null, Zval? trace = null) {
            base(new ClassMetadata(normalize_php_exception_class(class_name)));
            this.php_message = message;
            this.php_code = code;
            this.file_name = file_name;
            this.line = line;
            this.previous = previous;
            this.php_trace = trace ?? Zval.from_array({});
            set_php_property("message", message);
            set_php_property("code", code);
            set_php_property("trace", this.php_trace);
            if (file_name != "") {
                set_php_property("file", Zval.from_string(file_name));
            }
            if (line > 0) {
                set_php_property("line", Zval.from_int(line));
            }
            if (previous != null) {
                set_php_property("previous", previous);
            }
        }

        public void update_php_trace(Zval trace) {
            this.php_trace = trace;
            set_php_property("trace", trace);
        }

        public string php_class_name() {
            return class_metadata.php_name;
        }

        public bool is_a(string class_name) {
            var actual = normalize_php_exception_class(php_class_name());
            var wanted = normalize_php_exception_class(class_name);
            if (actual == wanted) {
                return true;
            }
            if (wanted == "Throwable") {
                return actual == "Throwable" || actual == "Exception" || actual == "RuntimeException" || actual == "UnexpectedValueException" || actual == "Error" || actual == "TypeError" || actual == "ArgumentCountError";
            }
            if (wanted == "Exception") {
                return actual == "RuntimeException" || actual == "UnexpectedValueException" || actual == "Exception";
            }
            if (wanted == "RuntimeException") {
                return actual == "UnexpectedValueException" || actual == "RuntimeException";
            }
            if (wanted == "Error") {
                return actual == "TypeError" || actual == "ArgumentCountError" || actual == "Error";
            }
            if (wanted == "TypeError") {
                return actual == "ArgumentCountError" || actual == "TypeError";
            }
            return false;
        }
    }

    public static Zval create_throwable_zval(string class_name, Zval message, Zval code, string file_name = "", int line = 0, Zval? previous = null, Zval? trace = null) {
        return Zval.from_object(new PhpThrowableObject(class_name, message, code, file_name, line, previous, trace));
    }

    public static string create_throwable_transport_message(string class_name, Zval message, Zval code, string file_name = "", int line = 0, Zval? previous = null) {
        ensure_throwable_payload_registry();
        var throwable = create_throwable_zval(class_name, message, code, file_name, line, previous);
        var token = next_throwable_token.to_string();
        next_throwable_token++;
        throwable_payloads.replace(token, throwable);
        var object_payload = throwable_object_from_zval(throwable);
        var display_class = object_payload != null ? object_payload.php_class_name() : normalize_php_exception_class(class_name);
        return THROWABLE_REF_PREFIX + token + "\n" + display_class + ": " + message.to_php_string();
    }

    public static Zval? exception_throwable_zval(Error e) {
        var token = throwable_token_from_error(e);
        if (token == "" || throwable_payloads == null) {
            return null;
        }
        return throwable_payloads.lookup(token);
    }

    public static PhpThrowableObject? exception_throwable_object(Error e) {
        var throwable = exception_throwable_zval(e);
        if (throwable == null) {
            return null;
        }
        return throwable_object_from_zval(throwable);
    }

    public static void set_throwable_trace_from_transport_message(string message, Zval trace) {
        var token = throwable_token_from_transport_message(message);
        if (token == "" || throwable_payloads == null) return;
        var throwable = throwable_payloads.lookup(token);
        var object_payload = throwable != null ? throwable_object_from_zval(throwable) : null;
        if (object_payload != null) object_payload.update_php_trace(trace);
    }

    public static string throwable_display_text(string message) {
        if (!message.has_prefix(THROWABLE_REF_PREFIX)) {
            return message;
        }
        var lines = message.split("\n", 2);
        return lines.length > 1 ? lines[1] : "";
    }

    public static string throwable_display_text_from_error(Error e) {
        return throwable_display_text(e.message ?? "");
    }

    public static string throwable_class_name_from_error(Error e) {
        var throwable = exception_throwable_object(e);
        if (throwable != null) {
            return throwable.php_class_name();
        }
        var display = throwable_display_text_from_error(e);
        var first_line = display.split("\n", 2)[0];
        var colon = first_line.index_of(":");
        return colon >= 0 ? normalize_php_exception_class(first_line.substring(0, colon)) : "Exception";
    }

    public static Zval throwable_message_from_error(Error e) {
        var throwable = exception_throwable_object(e);
        if (throwable != null) {
            return throwable.php_message;
        }
        var display = throwable_display_text_from_error(e);
        var first_line = display.split("\n", 2)[0];
        var colon = first_line.index_of(":");
        return Zval.from_string(colon >= 0 ? first_line.substring(colon + 2) : first_line);
    }

    public static Zval throwable_code_from_error(Error e) {
        var throwable = exception_throwable_object(e);
        if (throwable != null) {
            return throwable.php_code;
        }
        return Zval.from_int(0);
    }
}

namespace PhpefRuntimeCore {
    public static string create_throwable_transport_message_from_zval(Zval throwable) {
        ensure_throwable_payload_registry();
        var token = next_throwable_token.to_string();
        next_throwable_token++;
        throwable_payloads.replace(token, throwable);
        var object_payload = throwable_object_from_zval(throwable);
        var display_class = object_payload != null ? object_payload.php_class_name() : "Exception";
        var display_message = object_payload != null ? object_payload.php_message.to_php_string() : "Object of class Throwable could not be converted to string";
        return THROWABLE_REF_PREFIX + token + "\n" + display_class + ": " + display_message;
    }

    public static bool throwable_zval_is_a(Zval value, string class_name) {
        var throwable = throwable_object_from_zval(value);
        return throwable != null && throwable.is_a(class_name);
    }

    public static Zval throwable_get_message(Zval value) throws Error {
        var throwable = throwable_object_from_zval(value);
        if (throwable == null) {
            throw_exception("Error", Zval.from_string("Call to a member function getMessage() on non-Throwable"));
        }
        return throwable.php_message;
    }

    public static Zval throwable_get_code(Zval value) throws Error {
        var throwable = throwable_object_from_zval(value);
        if (throwable == null) {
            throw_exception("Error", Zval.from_string("Call to a member function getCode() on non-Throwable"));
        }
        return throwable.php_code;
    }

    public static Zval throwable_get_previous(Zval value) throws Error {
        var throwable = throwable_object_from_zval(value);
        if (throwable == null) {
            throw_exception("Error", Zval.from_string("Call to a member function getPrevious() on non-Throwable"));
        }
        return throwable.previous ?? Zval.from_null();
    }
}

namespace PhpefRuntimeCore {
    private static string throwable_token_from_transport_message(string message) {
        var first_line = message.split("\n", 2)[0];
        if (!first_line.has_prefix(THROWABLE_REF_PREFIX)) {
            return "";
        }
        return first_line.substring(THROWABLE_REF_PREFIX.length);
    }
}
