<?php

function fail($message): void {
    echo "error: " . $message . "\n";
    exit(1);
}

function parse_options($argv): array {
    $options = array();
    $expect = "";
    $first = true;

    foreach ($argv as $arg) {
        if ($first) {
            $first = false;
            continue;
        }
        if ($expect !== "") {
            $options[$expect] = $arg;
            $expect = "";
            continue;
        }
        if ($arg === "--input" || $arg === "-i") {
            $expect = "--input";
            continue;
        }
        if ($arg === "--output" || $arg === "-o") {
            $expect = "--output";
            continue;
        }
        if ($arg === "--php-namespace") {
            $expect = "--php-namespace";
            continue;
        }
        if ($arg === "--vala-namespace") {
            $expect = "--vala-namespace";
            continue;
        }
        if ($arg === "--source-namespace") {
            $expect = "--source-namespace";
            continue;
        }
        if ($arg === "--pkg") {
            $expect = "--pkg";
            continue;
        }
        if ($arg === "--help" || $arg === "-h") {
            echo "Usage: php2vala-bindgen --input FILE --output FILE --php-namespace NAME --vala-namespace NAME --pkg NAME\n";
            echo "       php2vala-bindgen --input FILE.vapi --output FILE.php --php-namespace NAME --pkg NAME [--source-namespace NAME]\n";
            exit(0);
        }
        fail("unknown argument " . $arg);
    }
    if ($expect !== "") {
        fail("missing value for " . $expect);
    }
    return $options;
}

function require_option($options, $name): string {
    if (!isset($options[$name]) || $options[$name] === "") {
        fail("missing required option " . $name);
    }
    return $options[$name];
}

function normalize_php_namespace($namespace): string {
    return str_replace("/", "\\", str_replace(".", "\\", $namespace));
}

function optional_option($options, $name): string {
    if (!isset($options[$name])) {
        return "";
    }
    return $options[$name];
}

function write_text_file($path, $contents): void {
    $output_dir = dirname($path);
    if ($output_dir !== "" && !is_dir($output_dir)) {
        mkdir($output_dir, 0777, true);
    }
    file_put_contents($path, $contents);
}

function first_token($text): string {
    $text = trim($text);
    $end = strlen($text);
    foreach (array(" ", "{", ":") as $marker) {
        $pos = strpos($text, $marker);
        if ($pos !== false && $pos < $end) {
            $end = $pos;
        }
    }
    return trim(substr($text, 0, $end));
}

function after_marker($text, $marker): string {
    $pos = strpos($text, $marker);
    if ($pos === false) {
        return "";
    }
    return trim(substr($text, $pos + strlen($marker)));
}

function split_text($separator, $text): array {
    $parts = array();
    if ($separator === "") {
        $parts[] = $text;
        return $parts;
    }

    $remaining = $text;
    while (true) {
        $pos = strpos($remaining, $separator);
        if ($pos === false) {
            $parts[] = $remaining;
            break;
        }
        $parts[] = substr($remaining, 0, $pos);
        $remaining = substr($remaining, $pos + strlen($separator));
    }
    return $parts;
}

function split_top_level($separator, $text): array {
    $parts = array();
    $current = "";
    $paren_depth = 0;
    $bracket_depth = 0;
    $in_string = false;
    $string_quote = "";

    for ($i = 0; $i < strlen($text); $i++) {
        $char = substr($text, $i, 1);
        if ($in_string) {
            $current .= $char;
            if ($char === $string_quote) {
                $in_string = false;
                $string_quote = "";
            }
            continue;
        }
        if ($char === "\"" || $char === "'") {
            $in_string = true;
            $string_quote = $char;
            $current .= $char;
            continue;
        }
        if ($char === "(") $paren_depth++;
        if ($char === ")" && $paren_depth > 0) $paren_depth--;
        if ($char === "[") $bracket_depth++;
        if ($char === "]" && $bracket_depth > 0) $bracket_depth--;
        if ($char === $separator && $paren_depth === 0 && $bracket_depth === 0) {
            $parts[] = $current;
            $current = "";
            continue;
        }
        $current .= $char;
    }
    $parts[] = $current;
    return $parts;
}

function strip_leading_vapi_attributes($text): string {
    $text = trim($text);
    while (str_starts_with($text, "[")) {
        $depth = 0;
        $end = false;
        for ($i = 0; $i < strlen($text); $i++) {
            $char = substr($text, $i, 1);
            if ($char === "[") $depth++;
            if ($char === "]") {
                $depth--;
                if ($depth === 0) {
                    $end = $i;
                    break;
                }
            }
        }
        if ($end === false) break;
        $text = trim(substr($text, $end + 1));
    }
    return $text;
}

function detect_vapi_namespace($contents): string {
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_line);
        $line = strip_leading_vapi_attributes($line);
        if (str_starts_with($line, "namespace ")) {
            return first_token(trim(substr($line, strlen("namespace "))));
        }
    }
    return "";
}

function is_vapi_input($input): bool {
    return str_ends_with($input, ".vapi");
}


function normalize_vapi_type($type): string {
    $type = trim($type);
    $changed = true;
    while ($changed) {
        $changed = false;
        foreach (array("owned ", "unowned ", "weak ", "virtual ", "abstract ", "override ", "static ", "new ") as $prefix) {
            if (str_starts_with($type, $prefix)) {
                $type = trim(substr($type, strlen($prefix)));
                $changed = true;
            }
        }
    }
    if (str_starts_with($type, "global::")) {
        $type = substr($type, strlen("global::"));
    }
    return trim($type);
}

function vapi_default_for_php($raw_default): string {
    $normalized_default = trim($raw_default);
    if ($normalized_default === "null" || $normalized_default === "true" || $normalized_default === "false" || $normalized_default === "0" || $normalized_default === "1") {
        return " = " . $normalized_default;
    }
    if (str_contains($normalized_default, ".")) {
        return " = 0";
    }
    return "";
}

function starts_with_upper_ascii($text): bool {
    if ($text === "") return false;
    $first = substr($text, 0, 1);
    return $first >= "A" && $first <= "Z";
}

function normalize_vapi_object_type($type, $source_namespace): string {
    $type = normalize_vapi_type($type);
    if (str_ends_with($type, "?")) {
        $type = substr($type, 0, strlen($type) - 1);
    }
    if ($source_namespace !== "" && !str_contains($type, ".")) {
        $type = $source_namespace . "." . $type;
    }
    return $type;
}

function php_class_type_from_vapi_object($type, $php_namespace, $source_namespace): string {
    $qualified_type = normalize_vapi_object_type($type, $source_namespace);
    return "\\" . $php_namespace . "\\" . php_identifier_from_vala_name(str_replace(".", "_", $qualified_type));
}

function php_type_from_vapi($source_type, $php_namespace, $source_namespace): string {
    $type = normalize_vapi_type($source_type);
    $nullable = false;
    if (str_ends_with($type, "?")) {
        $nullable = true;
        $type = substr($type, 0, strlen($type) - 1);
    }
    if ($type === "void") return "void";
    if ($type === "string") return $nullable ? "?string" : "string";
    if ($type === "int" || $type === "int32" || $type === "int64") return "int";
    if ($type === "uint" || $type === "uint32" || $type === "uint64" || $type === "size_t") return "object";
    if ($type === "bool") return "bool";
    if ($type === "double" || $type === "float") return "float";
    if (str_contains($type, "[") || str_contains($type, "]")) return "array";
    if (str_ends_with($type, "Flags") || str_ends_with($type, "Enum")) return "int";
    if (str_contains($type, ".")) {
        $prefix = $source_namespace !== "" ? $source_namespace . "." : "";
        if ($prefix !== "" && str_starts_with($type, $prefix)) {
            return php_class_type_from_vapi_object($type, $php_namespace, $source_namespace);
        }
        return "object";
    }
    return "int";
}

function is_supported_stub_php_type($php_type): bool {
    return $php_type === "void" || $php_type === "int" || $php_type === "bool" || $php_type === "string" || $php_type === "?string" || str_starts_with($php_type, "\\");
}

function is_supported_stub_php_return_type($php_type): bool {
    return $php_type === "void" || $php_type === "int" || $php_type === "bool" || $php_type === "string" || $php_type === "?string";
}

function render_one_vapi_param_for_php($raw_param, $php_namespace, $source_namespace): string {
    $param_part = strip_leading_vapi_attributes($raw_param);
    if ($param_part === "" || str_starts_with($param_part, "out ") || str_starts_with($param_part, "ref ")) {
        return "__UNSUPPORTED__";
    }

    $default_text = "";
    $eq = -1;
    for ($param_eq_index = 0; $param_eq_index < strlen($param_part); $param_eq_index++) {
        $param_eq_char = substr($param_part, $param_eq_index, 1);
        if ($param_eq_char === "=" && $eq < 0) {
            $eq = $param_eq_index;
        }
    }
    if ($eq >= 0) {
        $default_text = vapi_default_for_php(substr($param_part, $eq + 1));
        $param_part = trim(substr($param_part, 0, $eq));
    }
    if (str_contains($param_part, "*") || str_contains($param_part, "[") || str_contains($param_part, "]")) {
        return $default_text !== "" ? "" : "__UNSUPPORTED__";
    }

    $last_space = -1;
    for ($param_char_index = 0; $param_char_index < strlen($param_part); $param_char_index++) {
        $param_char = substr($param_part, $param_char_index, 1);
        if ($param_char === " " || $param_char === "\t") {
            $last_space = $param_char_index;
        }
    }
    if ($last_space < 0) {
        return "";
    }

    $name = php_param_name(substr($param_part, $last_space + 1));
    $type_text = trim(substr($param_part, 0, $last_space));
    if ($type_text === "") {
        return "";
    }
    $php_type = php_type_from_vapi($type_text, $php_namespace, $source_namespace);
    if (!is_supported_stub_php_type($php_type) || $php_type === "void") {
        return $default_text !== "" ? "" : "__UNSUPPORTED__";
    }
    return $php_type . " $" . $name . $default_text;
}

function render_vapi_params_for_php($raw_params, $php_namespace, $source_namespace): string {
    $result = array();
    $raw_params = trim($raw_params);
    if ($raw_params === "") {
        return "";
    }

    $current_param = "";
    $paren_depth = 0;
    $bracket_depth = 0;
    $in_string = false;
    $string_quote = "";
    for ($param_scan_index = 0; $param_scan_index <= strlen($raw_params); $param_scan_index++) {
        $is_end = $param_scan_index >= strlen($raw_params);
        $param_scan_char = "";
        if (!$is_end) {
            $param_scan_char = substr($raw_params, $param_scan_index, 1);
        }

        if (!$is_end && $in_string) {
            $current_param .= $param_scan_char;
            if ($param_scan_char === $string_quote) {
                $in_string = false;
                $string_quote = "";
            }
            continue;
        }
        if (!$is_end && ($param_scan_char === "\"" || $param_scan_char === "'")) {
            $in_string = true;
            $string_quote = $param_scan_char;
            $current_param .= $param_scan_char;
            continue;
        }

        if ($is_end || ($param_scan_char === "," && $paren_depth === 0 && $bracket_depth === 0)) {
            $rendered_param = render_one_vapi_param_for_php(trim($current_param), $php_namespace, $source_namespace);
            if ($rendered_param === "__UNSUPPORTED__") {
                return "__UNSUPPORTED__";
            }
            if ($rendered_param !== "") {
                $result[] = $rendered_param;
            }
            $current_param = "";
            continue;
        }

        if ($param_scan_char === "(") $paren_depth++;
        if ($param_scan_char === ")" && $paren_depth > 0) $paren_depth--;
        if ($param_scan_char === "[") $bracket_depth++;
        if ($param_scan_char === "]" && $bracket_depth > 0) $bracket_depth--;
        $current_param .= $param_scan_char;
    }
    return implode(", ", $result);
}

function render_vapi_callable_for_php($line, $kind, $class_name, $php_namespace, $source_namespace): string {
    if (!str_ends_with($line, ";") || !str_starts_with($line, "public ") || !str_contains($line, "(") || !str_contains($line, ")")) {
        return "";
    }
    if ($kind !== "signal" && (str_contains($line, " signal ") || str_contains($line, " static ") || str_contains($line, " class ") || str_contains($line, " async ") || str_contains($line, " throws "))) {
        return "";
    }
    if ($kind === "signal" && (str_contains($line, " static ") || str_contains($line, " class ") || str_contains($line, " async ") || str_contains($line, " throws "))) {
        return "";
    }
    if (str_contains($line, "void *") || str_contains($line, "void*")) {
        return "";
    }

    $line = substr($line, 0, strlen($line) - 1);
    $open = -1;
    $close = -1;
    for ($callable_char_index = 0; $callable_char_index < strlen($line); $callable_char_index++) {
        $callable_char = substr($line, $callable_char_index, 1);
        if ($callable_char === "(" && $open < 0) {
            $open = $callable_char_index;
        }
        if ($callable_char === ")") {
            $close = $callable_char_index;
        }
    }
    if ($open < 0 || $close < 0 || $close < $open) {
        return "";
    }
    $before = trim(substr($line, 0, $open));
    $params = substr($line, $open + 1, $close - $open - 1);
    $before = trim(substr($before, strlen("public ")));

    if ($kind === "constructor") {
        $rendered_params = render_vapi_params_for_php($params, $php_namespace, $source_namespace);
        if ($rendered_params === "__UNSUPPORTED__") {
            return "";
        }
        return "    /**\n     * @vala-constructor __construct\n     */\n    public function __construct(" . $rendered_params . ") {}\n\n";
    }

    $words = array();
    foreach (split_text(" ", $before) as $raw_callable_word) {
        $trimmed_callable_word = trim($raw_callable_word);
        if ($trimmed_callable_word !== "") {
            $words[] = $trimmed_callable_word;
        }
    }
    if (count($words) < 2) {
        return "";
    }
    if ($kind === "signal") {
        $raw_signal_name = $words[count($words) - 1];
        if (is_php_reserved_identifier($raw_signal_name)) {
            return "";
        }
        $signal_return_type = "";
        foreach ($words as $signal_word_index => $signal_word) {
            if ($signal_word_index >= count($words) - 1) continue;
            if ($signal_word === "virtual" || $signal_word === "abstract" || $signal_word === "override" || $signal_word === "signal") continue;
            $signal_return_type = $signal_word;
        }
        if ($signal_return_type !== "void") {
            return "";
        }
        $signal_name = php_identifier_from_vala_name($raw_signal_name);
        return "    /**\n     * @vala-signal " . $signal_name . "\n     */\n    public function on_" . $signal_name . "(callable " . "$" . "callback): void {}\n\n";
    }
    $raw_name_word = $words[count($words) - 1];
    if (str_starts_with($raw_name_word, "@")) {
        return "";
    }
    if (is_php_reserved_identifier($raw_name_word)) {
        return "";
    }
    $name = php_identifier_from_vala_name($raw_name_word);
    $return_parts = array();
    foreach ($words as $return_word_index => $return_type_word) {
        if ($return_word_index < count($words) - 1) {
            $return_parts[] = $return_type_word;
        }
    }
    $return_type = php_type_from_vapi(implode(" ", $return_parts), $php_namespace, $source_namespace);
    if (!is_supported_stub_php_return_type($return_type)) {
        return "";
    }
    $rendered_params = render_vapi_params_for_php($params, $php_namespace, $source_namespace);
    if ($rendered_params === "__UNSUPPORTED__") {
        return "";
    }
    return "    /**\n     * @vala-method " . $name . "\n     */\n    public function " . $name . "(" . $rendered_params . "): " . $return_type . " {}\n\n";
}

function render_vapi_top_level_function_for_php($line, $current_namespace, $php_namespace, $source_namespace): string {
    if (!str_ends_with($line, ";") || !str_starts_with($line, "public ") || !str_contains($line, "(") || !str_contains($line, ")")) {
        return "";
    }
    if (str_contains($line, " signal ") || str_contains($line, " delegate ") || str_contains($line, " class ") || str_contains($line, " async ") || str_contains($line, " throws ")) {
        return "";
    }
    if (str_contains($line, "void *") || str_contains($line, "void*")) {
        return "";
    }

    $line = substr($line, 0, strlen($line) - 1);
    $open = -1;
    $close = -1;
    for ($callable_char_index = 0; $callable_char_index < strlen($line); $callable_char_index++) {
        $callable_char = substr($line, $callable_char_index, 1);
        if ($callable_char === "(" && $open < 0) {
            $open = $callable_char_index;
        }
        if ($callable_char === ")") {
            $close = $callable_char_index;
        }
    }
    if ($open < 0 || $close < 0 || $close < $open) {
        return "";
    }

    $before = trim(substr($line, 0, $open));
    $params = substr($line, $open + 1, $close - $open - 1);
    $before = trim(substr($before, strlen("public ")));
    if (str_starts_with($before, "static ")) {
        $before = trim(substr($before, strlen("static ")));
    }

    $words = array();
    foreach (split_text(" ", $before) as $raw_callable_word) {
        $trimmed_callable_word = trim($raw_callable_word);
        if ($trimmed_callable_word !== "") {
            $words[] = $trimmed_callable_word;
        }
    }
    if (count($words) < 2) {
        return "";
    }

    $raw_name_word = $words[count($words) - 1];
    if (str_starts_with($raw_name_word, "@") || is_php_reserved_identifier($raw_name_word)) {
        return "";
    }

    $return_parts = array();
    foreach ($words as $return_word_index => $return_type_word) {
        if ($return_word_index < count($words) - 1) {
            $return_parts[] = $return_type_word;
        }
    }
    $return_type = php_type_from_vapi(implode(" ", $return_parts), $php_namespace, $source_namespace);
    if (!is_supported_stub_php_type($return_type)) {
        return "";
    }

    $rendered_params = render_vapi_params_for_php($params, $php_namespace, $source_namespace);
    if ($rendered_params === "__UNSUPPORTED__") {
        return "";
    }
    if (str_contains($return_type, "\\") || str_contains($rendered_params, "\\")) {
        return "";
    }

    $php_prefix = $current_namespace !== "" ? $current_namespace . "_" : "";
    $php_name = php_identifier_from_vala_name($php_prefix . $raw_name_word);
    $vala_namespace = $current_namespace !== "" ? $current_namespace : $source_namespace;
    $vala_name = $vala_namespace . "." . $raw_name_word;

    return "/**\n * @vala-function " . $vala_name . "\n */\nfunction " . $php_name . "(" . $rendered_params . "): " . $return_type . " {}\n\n";
}

function is_identifier_text($text): bool {
    $text = trim($text);
    if ($text === "") return false;
    $first = substr($text, 0, 1);
    if (!str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_", $first)) return false;
    for ($i = 1; $i < strlen($text); $i++) {
        $char = substr($text, $i, 1);
        if (!str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", $char)) return false;
    }
    return true;
}

function is_php_reserved_identifier($name): bool {
    return in_array(strtolower($name), array(
        "abstract", "and", "array", "as", "break", "callable", "case", "catch",
        "class", "clone", "const", "continue", "declare", "default", "do",
        "echo", "else", "elseif", "empty", "enddeclare", "endfor", "endforeach",
        "endif", "endswitch", "endwhile", "eval", "exit", "extends", "final",
        "finally", "fn", "for", "foreach", "function", "global", "goto", "if",
        "implements", "include", "include_once", "instanceof", "insteadof",
        "interface", "isset", "list", "match", "namespace", "new", "or",
        "print", "private", "protected", "public", "readonly", "require",
        "require_once", "return", "static", "switch", "throw", "trait", "try",
        "unset", "use", "var", "while", "xor", "yield"
    ), true);
}

function php_identifier_from_vala_name($name): string {
    $name = trim($name);
    if (str_starts_with($name, "@")) {
        $name = substr($name, 1);
    }
    $normalized = "";
    for ($i = 0; $i < strlen($name); $i++) {
        $char = substr($name, $i, 1);
        if (str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", $char)) {
            $normalized .= $char;
        } else {
            $normalized .= "_";
        }
    }
    $name = $normalized;
    if ($name === "") {
        $name = "_";
    }
    $first = substr($name, 0, 1);
    if (!str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_", $first)) {
        $name = "_" . $name;
    }
    if (is_php_reserved_identifier($name)) {
        $name .= "_";
    }
    return php_param_name($name);
}

function words_from_text($text): array {
    $words = array();
    $current = "";
    for ($i = 0; $i < strlen($text); $i++) {
        $char = substr($text, $i, 1);
        if ($char === " " || $char === "\t" || $char === "\n" || $char === "\r") {
            if ($current !== "") {
                $words[] = $current;
                $current = "";
            }
            continue;
        }
        $current .= $char;
    }
    if ($current !== "") $words[] = $current;
    return $words;
}

function attr_string_value($line, $name): string {
    $marker = $name . " = \"";
    $pos = strpos($line, $marker);
    if ($pos === false) return "";
    $start = $pos + strlen($marker);
    $tail = substr($line, $start);
    $end = strpos($tail, "\"");
    if ($end === false) return "";
    return substr($tail, 0, $end);
}

function detect_vapi_header_filename($contents): string {
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_line);
        if (str_starts_with($line, "[CCode") && str_contains($line, "cheader_filename")) return attr_string_value($line, "cheader_filename");
    }
    return "";
}

function parse_c_define_constants($header_filename): array {
    $values = array();
    if ($header_filename === "") return $values;
    $header_path = $header_filename;
    if (!str_starts_with($header_path, "/")) $header_path = "/usr/include/" . $header_filename;
    if (!is_file($header_path)) return $values;
    $contents = file_get_contents($header_path);
    if ($contents === false) return $values;
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_line);
        $words = words_from_text($line);
        if (count($words) < 3 || $words[0] !== "#define") continue;
        $name = $words[1];
        $value = $words[2];
        if (str_starts_with($name, "MAGIC_") && (str_starts_with($value, "0x") || str_starts_with($value, "0X") || is_numeric($value))) {
            $values[$name] = $value;
        }
    }
    return $values;
}

function enum_constant_value($c_constants, $cprefix, $name): string {
    $key = $cprefix . $name;
    if (isset($c_constants[$key])) return $c_constants[$key];
    if ($key === "MAGIC_NO_CHECK_ATYPE" && isset($c_constants["MAGIC_NO_CHECK_APPTYPE"])) return $c_constants["MAGIC_NO_CHECK_APPTYPE"];
    if ($key === "MAGIC_NO_CHECK_ASCII" && isset($c_constants["MAGIC_NO_CHECK_TEXT"])) return $c_constants["MAGIC_NO_CHECK_TEXT"];
    if ($key === "MAGIC_NO_CHECK_FORTRAN" && isset($c_constants["MAGIC_NO_CHECK_TEXT"])) return $c_constants["MAGIC_NO_CHECK_TEXT"];
    if ($key === "MAGIC_NO_CHECK_TROFF" && isset($c_constants["MAGIC_NO_CHECK_TEXT"])) return $c_constants["MAGIC_NO_CHECK_TEXT"];
    return "0";
}

function render_enum_constants_class($class_name, $names, $c_constants, $cprefix): string {
    if ($class_name === "" || count($names) === 0) return "";
    $class_name = php_identifier_from_vala_name($class_name);
    $stub = "class " . $class_name . " {\n";
    foreach ($names as $name) {
        $php_name = php_identifier_from_vala_name($name);
        if (is_identifier_text($php_name)) $stub .= "    public const " . $php_name . " = " . enum_constant_value($c_constants, $cprefix, $name) . ";\n";
    }
    $stub .= "}\n\n";
    return $stub;
}

function add_enum_names_from_text($text, &$names): void {
    $clean = str_replace("{", " ", str_replace("}", " ", $text));
    foreach (split_text(",", $clean) as $part) {
        $name = trim($part);
        if ($name === "" || str_starts_with($name, "public enum ")) continue;
        if (str_contains($name, "(") || str_starts_with($name, "public ")) continue;
        if (str_contains($name, ";")) $name = trim(substr($name, 0, strpos($name, ";")));
        if (str_contains($name, "=")) $name = trim(substr($name, 0, strpos($name, "=")));
        if (is_identifier_text($name)) $names[] = $name;
    }
}

function render_vapi_enums_for_php($contents): string {
    $stub = "";
    $c_constants = parse_c_define_constants(detect_vapi_header_filename($contents));
    $pending_cprefix = "";
    $enum_open = false;
    $enum_name = "";
    $enum_cprefix = "";
    $enum_names = array();
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_line);
        if ($line === "" || str_starts_with($line, "//") || str_starts_with($line, "/*") || str_starts_with($line, "*")) continue;
        if (!$enum_open && str_starts_with($line, "[CCode") && str_contains($line, "cprefix")) {
            $pending_cprefix = attr_string_value($line, "cprefix");
            continue;
        }
        if (!$enum_open && str_starts_with($line, "public enum ")) {
            $rest = trim(substr($line, strlen("public enum ")));
            $enum_name = first_token($rest);
            $enum_cprefix = $pending_cprefix;
            $pending_cprefix = "";
            $enum_names = array();
            $enum_open = true;
            $brace = strpos($line, "{");
            if ($brace !== false) add_enum_names_from_text(substr($line, $brace + 1), $enum_names);
            if (str_contains($line, "}")) {
                $stub .= render_enum_constants_class($enum_name, $enum_names, $c_constants, $enum_cprefix);
                $enum_open = false;
            }
            continue;
        }
        if ($enum_open) {
            add_enum_names_from_text($line, $enum_names);
            if (str_contains($line, "}")) {
                $stub .= render_enum_constants_class($enum_name, $enum_names, $c_constants, $enum_cprefix);
                $enum_open = false;
            }
        }
    }
    return $stub;
}

function render_pkg_lifecycle_helpers($pkg): string {
    return "";
}

function vapi_qualified_class_name($namespace, $class_name): string {
    return ($namespace !== "" ? $namespace . "." : "") . $class_name;
}

function parse_vapi_class_bases($line): array {
    $colon = strpos($line, ":");
    $brace = strpos($line, "{");
    if ($colon === false || ($brace !== false && $colon > $brace)) {
        return array();
    }
    $base_part = substr($line, $colon + 1);
    if ($brace !== false) {
        $base_part = substr($base_part, 0, strpos($base_part, "{"));
    }
    $bases = array();
    foreach (split_text(",", $base_part) as $raw_base) {
        $base = first_token(trim($raw_base));
        if ($base !== "") {
            $bases[] = normalize_vapi_type($base);
        }
    }
    return $bases;
}

function collect_vapi_class_members($contents, $source_namespace): array {
    $classes = array();
    $current_namespace = "";
    $namespace_depth = 0;
    $depth = 0;
    $class_open = false;
    $class_depth = 0;
    $class_name = "";
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_line);
        $line = strip_leading_vapi_attributes($line);
        if ($depth === 0 && str_starts_with($line, "namespace ") && str_contains($line, "{")) {
            $current_namespace = first_token(trim(substr($line, strlen("namespace "))));
            $namespace_depth = $depth + 1;
        }
        if ($line === "" || str_starts_with($line, "[") || str_starts_with($line, "/*") || str_starts_with($line, "//") || str_starts_with($line, "*")) {
            continue;
        }
        $opened_class = false;
        if (!$class_open && str_starts_with($line, "public ") && (str_contains($line, " class ") || str_contains($line, " interface ") || str_contains($line, " struct ")) && str_contains($line, "{")) {
            $rest = "";
            if (str_contains($line, " class ")) {
                $rest = after_marker($line, " class ");
            }
            if ($rest === "" && str_contains($line, " interface ")) {
                $rest = after_marker($line, " interface ");
            }
            if ($rest === "" && str_contains($line, " struct ")) {
                $rest = after_marker($line, " struct ");
            }
            $class_name = first_token($rest);
            $class_base_types = parse_vapi_class_bases($line);
            $class_namespace = $current_namespace !== "" ? $current_namespace : $source_namespace;
            $qualified = vapi_qualified_class_name($class_namespace, $class_name);
            if (!isset($classes[$qualified])) {
                $classes[$qualified] = array(array(), array());
            }
            $classes[$qualified][0] = parse_vapi_class_bases($line);
            $class_open = true;
            $class_depth = $depth + 1;
            $opened_class = true;
        }
        if (!$opened_class && $class_open && $depth >= $class_depth && str_starts_with($line, "public ")) {
            $qualified = vapi_qualified_class_name($current_namespace !== "" ? $current_namespace : $source_namespace, $class_name);
            $class_members = $classes[$qualified][1];
            $class_members[] = $line;
            $classes[$qualified][1] = $class_members;
        }
        for ($i = 0; $i < strlen($line); $i++) {
            $char = substr($line, $i, 1);
            if ($char === "{") {
                $depth++;
            }
            if ($char === "}") {
                $depth--;
            }
        }
        if ($class_open && $depth < $class_depth) {
            $class_open = false;
            $class_depth = 0;
            $class_name = "";
        }
        if ($namespace_depth > 0 && $depth < $namespace_depth) {
            $current_namespace = "";
            $namespace_depth = 0;
        }
    }
    return $classes;
}

function collect_vapi_class_names($contents, $source_namespace): array {
    $members = collect_vapi_class_members($contents, $source_namespace);
    $names = array();
    foreach ($members as $qualified => $_) {
        $names[$qualified] = true;
    }
    return $names;
}

function vapi_search_dirs(): array {
    $dirs = array("/usr/share/vala-0.56/vapi", "/usr/share/vala/vapi");
    $result = array();
    foreach ($dirs as $dir) {
        if (is_dir($dir)) {
            $result[] = $dir;
        }
    }
    return $result;
}

function collect_vapi_search_class_members($fallback_namespace): array {
    $result = array();
    foreach (vapi_search_dirs() as $dir) {
        foreach (glob($dir . "/*.vapi") as $path) {
            $contents = file_get_contents($path);
            if ($contents === false) continue;
            $source_namespace = detect_vapi_namespace($contents);
            if ($source_namespace === "") $source_namespace = $fallback_namespace;
            $classes = collect_vapi_class_members($contents, $source_namespace);
            foreach ($classes as $qualified => $info) {
                if (!isset($result[$qualified])) {
                    $result[$qualified] = $info;
                }
            }
        }
    }
    return $result;
}

function qualify_vapi_base_type($base_type, $current_namespace): string {
    $base_type = normalize_vapi_type($base_type);
    if ($base_type === "") return "";
    if (str_contains($base_type, ".")) return $base_type;
    return vapi_qualified_class_name($current_namespace, $base_type);
}

function php_class_name_from_qualified_vapi_type($qualified_type): string {
    return php_identifier_from_vala_name(str_replace(".", "_", $qualified_type));
}

function choose_php_extends_name_for_vapi_bases($base_types, $source_namespace, $emitted_php_class_names): string {
    foreach ($base_types as $base_type) {
        $qualified = qualify_vapi_base_type($base_type, $source_namespace);
        if ($qualified === "") continue;
        $candidate = php_class_name_from_qualified_vapi_type($qualified);
        if (isset($emitted_php_class_names[$candidate])) {
            return $candidate;
        }
    }
    return "";
}

function vapi_callable_php_name($line, $kind, $class_name): string {
    if (!str_contains($line, "(")) return "";
    $before_paren = trim(substr($line, 0, strpos($line, "(")));
    $words = array();
    foreach (split_text(" ", $before_paren) as $raw_word) {
        $word = trim($raw_word);
        if ($word === "" || $word === "public" || $word === "static" || $word === "virtual" || $word === "abstract" || $word === "override" || $word === "signal") continue;
        $words[] = $word;
    }
    if ($kind === "constructor") return "__construct";
    if (count($words) === 0) return "";
    $name = php_identifier_from_vala_name($words[count($words) - 1]);
    if ($kind === "signal") return "on_" . $name;
    return $name;
}

function vapi_callable_has_unsupported_surface($line): bool {
    if (str_contains($line, "...")) return true;
    if (str_contains($line, "va_list")) return true;
    if (str_contains($line, "<") || str_contains($line, ">")) return true;
    return false;
}

function is_unfriendly_inherited_method_name($name): bool {
    if ($name === "activate") return true;
    if ($name === "add_toggle_ref") return true;
    if ($name === "bind_property") return true;
    if ($name === "bind_property_full") return true;
    if ($name === "constructed") return true;
    if ($name === "disconnect") return true;
    if ($name === "dispose") return true;
    if ($name === "dup_data_T_") return true;
    if ($name === "dup_qdata_T_") return true;
    if ($name === "finalize") return true;
    if ($name === "force_floating") return true;
    if ($name === "freeze_notify") return true;
    if ($name === "get_data") return true;
    if ($name === "get_data_T_") return true;
    if ($name === "get_qdata") return true;
    if ($name === "get_qdata_T_") return true;
    if ($name === "getv") return true;
    if ($name === "notify_property") return true;
    if ($name === "ref") return true;
    if ($name === "ref_sink") return true;
    if ($name === "remove_toggle_ref") return true;
    if ($name === "replace_data") return true;
    if ($name === "replace_qdata") return true;
    if ($name === "run_dispose") return true;
    if ($name === "set_data") return true;
    if ($name === "set_data_T_") return true;
    if ($name === "set_qdata") return true;
    if ($name === "set_qdata_T_") return true;
    if ($name === "set_valist") return true;
    if ($name === "setv") return true;
    if ($name === "steal_data") return true;
    if ($name === "steal_data_T_") return true;
    if ($name === "steal_qdata") return true;
    if ($name === "steal_qdata_T_") return true;
    if ($name === "thaw_notify") return true;
    if ($name === "unref") return true;
    if ($name === "watch_closure") return true;
    if ($name === "weak_ref") return true;
    if ($name === "weak_unref") return true;
    return false;
}
function vapi_class_has_method_name($info, $method_name): bool {
    foreach ($info[1] as $candidate_line) {
        if (!str_contains($candidate_line, "(")) continue;
        $candidate_before = trim(substr($candidate_line, 0, strpos($candidate_line, "(")));
        if (str_contains($candidate_before, " signal ")) continue;
        $candidate_name = vapi_callable_php_name($candidate_line, "method", "");
        if ($candidate_name === $method_name) return true;
    }
    return false;
}

function render_vapi_inherited_class_methods($base_types, $php_namespace, $source_namespace, $class_members, $emitted_names, $emitted_php_class_names): string {
    $rendered = "";
    $queue = $base_types;
    $visited = array();
    $emitted = $emitted_names;
    while (count($queue) > 0) {
        $base = array_shift($queue);
        $qualified = qualify_vapi_base_type($base, $source_namespace);
        if ($qualified === "" || isset($visited[$qualified])) continue;
        $visited[$qualified] = true;
        if (!isset($class_members[$qualified])) continue;
        $info = $class_members[$qualified];
        foreach ($info[0] as $parent) {
            $queue[] = $parent;
        }
        $class_short_name = $qualified;
        if (str_contains($class_short_name, ".")) {
            $parts = split_text(".", $class_short_name);
            $class_short_name = $parts[count($parts) - 1];
        }
        foreach ($info[1] as $line) {
            if (!str_contains($line, "(")) continue;
            if (vapi_callable_has_unsupported_surface($line)) continue;
            $before_paren = trim(substr($line, 0, strpos($line, "(")));
            $is_ctor = str_ends_with($before_paren, " " . $class_short_name) || str_ends_with($before_paren, "." . $class_short_name);
            if ($is_ctor) continue;
            $is_signal = str_contains($before_paren, " signal ");
            $kind = $is_signal ? "signal" : "method";
            $callable_php_name = vapi_callable_php_name($line, $kind, $class_short_name);
            if ($callable_php_name === "") continue;
            if (is_unfriendly_inherited_method_name($callable_php_name)) continue;
            if ($kind === "signal") {
                $method_name = substr($callable_php_name, strlen("on_"));
                if (isset($emitted[$method_name])) continue;
                if (vapi_class_has_method_name($info, $method_name) && !is_unfriendly_inherited_method_name($method_name)) continue;
            }
            if ($kind === "method" && isset($emitted["on_" . $callable_php_name])) continue;
            if (isset($emitted[$callable_php_name])) continue;
            $render_namespace = $source_namespace;
            if (str_contains($qualified, ".")) {
                $qualified_parts = split_text(".", $qualified);
                $render_namespace = $qualified_parts[0];
            }
            $rendered_callable = render_vapi_callable_for_php($line, $kind, $class_short_name, $php_namespace, $render_namespace);
            if ($rendered_callable === "") continue;
            if (rendered_callable_uses_unavailable_php_type($rendered_callable, $php_namespace, $emitted_php_class_names)) continue;
            $emitted[$callable_php_name] = true;
            $rendered .= $rendered_callable;
        }
    }
    return $rendered;
}

function render_vapi_extra_class_methods($source_namespace, $current_namespace, $class_name): string {
    return "";
}

function collect_vapi_emitted_php_class_names($contents, $php_namespace, $source_namespace): array {
    $emitted = array();
    for ($pass = 0; $pass < 64; $pass++) {
        $next_emitted = array();
        $class_open = false;
        $class_name = "";
        $current_namespace = "";
        $namespace_depth = 0;
        $depth = 0;
        $class_depth = 0;
        $class_emit_allowed = false;
        $class_is_abstract_wrapper = false;
        $class_has_callable = false;
        $class_has_constructor = false;
        $skip_next_container = false;

        foreach (split_text("\n", $contents) as $raw_line) {
            if (str_contains($raw_line, "gsk/broadway/")) {
                $skip_next_container = true;
            }
            $line = trim($raw_line);
            $line = strip_leading_vapi_attributes($line);
            if ($depth === 0 && str_starts_with($line, "namespace ") && str_contains($line, "{")) {
                $current_namespace = first_token(trim(substr($line, strlen("namespace "))));
                $namespace_depth = $depth + 1;
            }
            if ($line === "" || str_starts_with($line, "[") || str_starts_with($line, "/*") || str_starts_with($line, "//") || str_starts_with($line, "*")) {
                continue;
            }

            $opened_container = false;
            if (!$class_open && str_starts_with($line, "public ") && (str_contains($line, " class ") || str_contains($line, " interface ") || str_contains($line, " struct ")) && str_contains($line, "{")) {
                $rest = "";
                if (str_contains($line, " class ")) {
                    $rest = after_marker($line, " class ");
                }
                if ($rest === "" && str_contains($line, " interface ")) {
                    $rest = after_marker($line, " interface ");
                }
                if ($rest === "" && str_contains($line, " struct ")) {
                    $rest = after_marker($line, " struct ");
                }
                $class_name = first_token($rest);
                $class_is_abstract_wrapper = str_contains($line, " abstract ");
                $class_emit_allowed = true;
                if (str_contains($line, " interface ")) {
                    $class_emit_allowed = false;
                }
                if ($skip_next_container) {
                    $class_emit_allowed = false;
                }
                $skip_next_container = false;
                $class_has_callable = false;
                $class_has_constructor = false;
                $class_open = true;
                $class_depth = $depth + 1;
                $opened_container = true;
            }

            if (!$opened_container && $class_open && $class_emit_allowed && $depth >= $class_depth && str_starts_with($line, "public ") && str_contains($line, "(")) {
                $before_paren = trim(substr($line, 0, strpos($line, "(")));
                $is_ctor = str_ends_with($before_paren, " " . $class_name) || str_ends_with($before_paren, "." . $class_name);
                $is_signal = str_contains($before_paren, " signal ");
                $callable_kind = $is_signal ? "signal" : ($is_ctor ? "constructor" : "method");
                $vala_class_namespace = $current_namespace !== "" ? $current_namespace : $source_namespace;
                $rendered_callable = render_vapi_callable_for_php($line, $callable_kind, $class_name, $php_namespace, $vala_class_namespace);
                if ($rendered_callable !== "") {
                    $callable_usable = true;
                    if ($pass > 0 && rendered_callable_uses_unavailable_php_type($rendered_callable, $php_namespace, $emitted)) {
                        $callable_usable = false;
                    }
                    if ($callable_usable) {
                        $class_has_callable = true;
                        if ($is_ctor) {
                            $class_has_constructor = true;
                        }
                    }
                }
            }

            for ($i = 0; $i < strlen($line); $i++) {
                $char = substr($line, $i, 1);
                if ($char === "{") {
                    $depth++;
                }
                if ($char === "}") {
                    $depth--;
                }
            }
            if ($class_open && $depth < $class_depth) {
                $class_can_emit = $class_has_constructor;
                if (!$class_can_emit && $class_is_abstract_wrapper) {
                    $class_can_emit = true;
                }
                if ($class_emit_allowed && $class_has_callable && $class_can_emit) {
                    $php_class_prefix = $current_namespace !== "" ? $current_namespace . "_" : "";
                    $next_emitted[php_identifier_from_vala_name($php_class_prefix . $class_name)] = true;
                }
                $class_open = false;
                $class_name = "";
                $class_depth = 0;
                $class_emit_allowed = false;
                $class_is_abstract_wrapper = false;
                $class_has_callable = false;
                $class_has_constructor = false;
            }
            if ($namespace_depth > 0 && $depth < $namespace_depth) {
                $current_namespace = "";
                $namespace_depth = 0;
            }
        }

        if ($pass > 0) {
            $same = true;
            foreach ($next_emitted as $name => $value) {
                if (!isset($emitted[$name])) {
                    $same = false;
                }
            }
            foreach ($emitted as $name => $value) {
                if (!isset($next_emitted[$name])) {
                    $same = false;
                }
            }
            if ($same) {
                return $next_emitted;
            }
        }
        $emitted = $next_emitted;
    }
    return $emitted;
}

function rendered_callable_uses_unavailable_php_type($rendered_callable, $php_namespace, $emitted_php_class_names): bool {
    $prefix = "\\" . $php_namespace . "\\";
    $offset = 0;
    while (true) {
        $pos = strpos($rendered_callable, $prefix, $offset);
        if ($pos === false) {
            return false;
        }
        $name_start = $pos + strlen($prefix);
        $name = "";
        for ($i = $name_start; $i < strlen($rendered_callable); $i++) {
            $char = substr($rendered_callable, $i, 1);
            if (str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", $char)) {
                $name .= $char;
                continue;
            }
            break;
        }
        if ($name !== "" && !isset($emitted_php_class_names[$name])) {
            return true;
        }
        $offset = $name_start + strlen($name);
    }
}

function generate_php_stub_from_vapi($contents, $php_namespace, $source_namespace, $pkg): string {
    if ($source_namespace === "") {
        $source_namespace = detect_vapi_namespace($contents);
    }
    if ($source_namespace === "") {
        fail("cannot detect source namespace from vapi; pass --source-namespace");
    }

    $stub = "<?php\n";
    $stub .= "/**\n";
    $stub .= " * Generated by php2vala-bindgen.\n";
    $stub .= " * @php-namespace " . $php_namespace . "\n";
    $stub .= " * @vala-namespace " . $source_namespace . "\n";
    $stub .= " * @pkg " . $pkg . "\n";
    $stub .= " */\n";
    $stub .= "namespace " . $php_namespace . ";\n\n";
    $stub .= render_vapi_enums_for_php($contents);
    $vapi_class_members = collect_vapi_class_members($contents, $source_namespace);
    $emitted_php_class_names = collect_vapi_emitted_php_class_names($contents, $php_namespace, $source_namespace);
    $external_vapi_class_members = collect_vapi_search_class_members($source_namespace);
    foreach ($external_vapi_class_members as $qualified_external_class => $external_class_info) {
        if (!isset($vapi_class_members[$qualified_external_class])) {
            $vapi_class_members[$qualified_external_class] = $external_class_info;
        }
    }

    $class_open = false;
    $class_name = "";
    $current_namespace = "";
    $namespace_depth = 0;
    $depth = 0;
    $class_depth = 0;
    $class_stub = "";
    $class_has_callable = false;
    $class_has_constructor = false;
    $class_base_types = array();
    $class_emit_allowed = false;
    $class_is_abstract_wrapper = false;
    $class_emitted_member_names = array();
    $skip_next_container = false;

    foreach (split_text("\n", $contents) as $raw_line) {
        if (str_contains($raw_line, "gsk/broadway/")) {
            $skip_next_container = true;
        }
        $line = trim($raw_line);
        $line = strip_leading_vapi_attributes($line);
        if ($depth === 0 && str_starts_with($line, "namespace ") && str_contains($line, "{")) {
            $current_namespace = first_token(trim(substr($line, strlen("namespace "))));
            $namespace_depth = $depth + 1;
        }
        if ($line === "" || str_starts_with($line, "[") || str_starts_with($line, "/*") || str_starts_with($line, "//") || str_starts_with($line, "*")) {
            continue;
        }

        $opened_container = false;
        if (!$class_open && str_starts_with($line, "public ") && (str_contains($line, " class ") || str_contains($line, " interface ") || str_contains($line, " struct ")) && str_contains($line, "{")) {
            $rest = "";
            if (str_contains($line, " class ")) {
                $rest = after_marker($line, " class ");
            }
            if ($rest === "" && str_contains($line, " interface ")) {
                $rest = after_marker($line, " interface ");
            }
            if ($rest === "" && str_contains($line, " struct ")) {
                $rest = after_marker($line, " struct ");
            }
            $class_name = first_token($rest);
            $class_base_types = parse_vapi_class_bases($line);
            $vala_class_namespace = $current_namespace !== "" ? $current_namespace : $source_namespace;
            $php_class_prefix = $current_namespace !== "" ? $current_namespace . "_" : "";
            $php_class_name = php_identifier_from_vala_name($php_class_prefix . $class_name);
            $php_class = "\\" . $php_namespace . "\\" . $php_class_name;
            $vala_class = $vala_class_namespace . "." . $class_name;
            $class_is_abstract_wrapper = str_contains($line, " abstract ");
            $class_emit_allowed = true;
            if (str_contains($line, " interface ")) {
                $class_emit_allowed = false;
            }
            if ($skip_next_container) {
                $class_emit_allowed = false;
            }
            $skip_next_container = false;
            $class_stub = "";
            $class_stub .= "/**\n";
            $class_stub .= " * @php " . $php_class . "\n";
            $class_stub .= " * @vala " . $vala_class . "\n";
            $class_stub .= " * @pkg " . $pkg . "\n";
            if ($class_is_abstract_wrapper) {
                $class_stub .= " * @php2vala-abstract-wrapper 1\n";
            }
            $class_stub .= " */\n";
            $php_extends = choose_php_extends_name_for_vapi_bases($class_base_types, $vala_class_namespace, $emitted_php_class_names);
            $class_stub .= "class " . $php_class_name . ($php_extends !== "" ? " extends " . $php_extends : "") . " {\n";
            $class_has_callable = false;
            $class_has_constructor = false;
            $class_emitted_member_names = array();
            $class_open = true;
            $class_depth = $depth + 1;
            $opened_container = true;
        }
        if (!$opened_container && $class_open && $class_emit_allowed && $depth >= $class_depth && str_starts_with($line, "public ")) {
            $before_paren = $line;
            if (str_contains($line, "(")) {
                $before_paren = trim(substr($line, 0, strpos($line, "(")));
            }
            if (vapi_callable_has_unsupported_surface($line)) {
                continue;
            }
            $is_ctor = str_ends_with($before_paren, " " . $class_name) || str_ends_with($before_paren, "." . $class_name);
            $is_signal = str_contains($before_paren, " signal ");
            $callable_kind = $is_signal ? "signal" : ($is_ctor ? "constructor" : "method");
            $rendered_callable = render_vapi_callable_for_php($line, $callable_kind, $class_name, $php_namespace, $vala_class_namespace);
            if ($rendered_callable !== "") {
                if (rendered_callable_uses_unavailable_php_type($rendered_callable, $php_namespace, $emitted_php_class_names)) {
                    continue;
                }
                $callable_php_name = vapi_callable_php_name($line, $callable_kind, $class_name);
                if ($callable_php_name !== "" && !isset($class_emitted_member_names[$callable_php_name])) {
                    $class_emitted_member_names[$callable_php_name] = true;
                    $class_has_callable = true;
                    if ($is_ctor) {
                        $class_has_constructor = true;
                    }
                    $class_stub .= $rendered_callable;
                }
            }
        }

        if (!$opened_container && !$class_open && $current_namespace !== "" && $depth === $namespace_depth && str_starts_with($line, "public ") && str_contains($line, "(")) {
            $rendered_function = render_vapi_top_level_function_for_php($line, $current_namespace, $php_namespace, $source_namespace);
            if ($rendered_function !== "") {
                $stub .= $rendered_function;
            }
        }

        for ($i = 0; $i < strlen($line); $i++) {
            $char = substr($line, $i, 1);
            if ($char === "{") {
                $depth++;
            }
            if ($char === "}") {
                $depth--;
            }
        }
        if ($class_open && $depth < $class_depth) {
            $class_can_emit = $class_has_constructor;
            if (!$class_can_emit && $class_is_abstract_wrapper) {
                $class_can_emit = true;
            }
            if ($class_emit_allowed && $class_has_callable && $class_can_emit) {
                $stub .= $class_stub;
                if ($php_extends === "") {
                    $stub .= render_vapi_inherited_class_methods($class_base_types, $php_namespace, $source_namespace, $vapi_class_members, $class_emitted_member_names, $emitted_php_class_names);
                }
                $stub .= render_vapi_extra_class_methods($source_namespace, $current_namespace, $class_name);
                $stub .= "}\n\n";
            }
            $class_open = false;
            $class_name = "";
            $class_depth = 0;
            $class_stub = "";
            $class_has_callable = false;
            $class_has_constructor = false;
            $class_base_types = array();
            $class_emit_allowed = false;
            $class_is_abstract_wrapper = false;
            $class_emitted_member_names = array();
        }
        if ($namespace_depth > 0 && $depth < $namespace_depth) {
            $current_namespace = "";
            $namespace_depth = 0;
        }
    }
    if ($class_open) {
        $class_can_emit = $class_has_constructor;
        if (!$class_can_emit && $class_is_abstract_wrapper) {
            $class_can_emit = true;
        }
        if ($class_emit_allowed && $class_has_callable && $class_can_emit) {
            $stub .= $class_stub;
            $stub .= render_vapi_extra_class_methods($source_namespace, $current_namespace, $class_name);
            $stub .= "}\n\n";
        }
    }
    $stub .= render_pkg_lifecycle_helpers($pkg);
    return $stub;
}

function clean_vala_type($type): string {
    $type = trim($type);
    foreach (array("owned ", "unowned ", "weak ", "ref ", "out ") as $prefix) {
        if (str_starts_with($type, $prefix)) {
            $type = trim(substr($type, strlen($prefix)));
        }
    }
    if (str_ends_with($type, "?")) {
        $type = substr($type, 0, strlen($type) - 1);
    }
    return trim($type);
}

function php_type($vala_type, $php_namespace, $vala_namespace): string {
    $type = clean_vala_type($vala_type);
    if ($type === "void") return "void";
    if ($type === "string") return "string";
    if ($type === "int" || $type === "uint" || $type === "int32" || $type === "uint32" || $type === "int64" || $type === "uint64") return "int";
    if ($type === "bool") return "bool";
    if ($type === "double" || $type === "float") return "float";
    if (str_ends_with($type, "[]") || $type === "GLib.List" || $type === "Gee.List") return "array";
    if (str_ends_with($type, "Callback")) return "callable";
    if (str_ends_with($type, "Flags") || str_ends_with($type, "Enum")) return "int";
    if (str_contains($type, ".")) {
        $prefix = $vala_namespace . ".";
        if ($vala_namespace !== "" && str_starts_with($type, $prefix)) {
            return php_class_type_from_vapi_object($type, $php_namespace, $vala_namespace);
        }
        return "object";
    }
    return "int";
}

function php_param_name($name): string {
    $name = str_replace("-", "_", $name);
    if (str_starts_with($name, "@")) {
        $name = substr($name, 1);
    }
    if ($name === "this" || $name === "class" || $name === "function") {
        return $name . "_";
    }
    return $name;
}

function render_params($raw, $php_namespace, $vala_namespace): string {
    $result = array();
    $raw = trim($raw);
    if ($raw === "") {
        return "";
    }
    foreach (split_text(",", $raw) as $raw_part) {
        $param_part = trim($raw_part);
        if ($param_part === "") {
            continue;
        }
        $default_text = "";
        $eq = strpos($param_part, "=");
        if ($eq !== false) {
            $default_value = trim(substr($param_part, $eq + 1));
            $param_part = trim(substr($param_part, 0, $eq));
            if ($default_value === "null" || $default_value === "true" || $default_value === "false" || $default_value === "0" || $default_value === "1") {
                $default_text = " = " . $default_value;
            }
        }
        $pieces = array();
        foreach (split_text(" ", $param_part) as $raw_piece) {
            $param_piece = trim($raw_piece);
            if ($param_piece !== "") {
                $pieces[] = $param_piece;
            }
        }
        if (count($pieces) < 2) {
            continue;
        }
        $name = php_param_name($pieces[count($pieces) - 1]);
        $type_parts = array();
        foreach ($pieces as $i => $type_piece) {
            if ($i < count($pieces) - 1) {
                $type_parts[] = $type_piece;
            }
        }
        $result[] = php_type(implode(" ", $type_parts), $php_namespace, $vala_namespace) . " $" . $name . $default_text;
    }
    return implode(", ", $result);
}

function render_callable($line, $kind, $class_name, $php_namespace, $vala_namespace): string {
    $open = strpos($line, "(");
    $close = strrpos($line, ")");
    if ($open === false || $close === false || $close < $open) {
        return "";
    }
    $before = trim(substr($line, 0, $open));
    $params = substr($line, $open + 1, $close - $open - 1);
    $words = array();
    foreach (split_text(" ", $before) as $raw_method_word) {
        $method_word = trim($raw_method_word);
        if ($method_word !== "" && $method_word !== "public" && $method_word !== "static" && $method_word !== "virtual" && $method_word !== "abstract" && $method_word !== "override") {
            $words[] = $method_word;
        }
    }
    if ($kind === "constructor") {
        return "    /** @vala-constructor __construct */\n    public function __construct(" . render_params($params, $php_namespace, $vala_namespace) . ") {}\n\n";
    }
    if ($kind === "signal") {
        if (count($words) < 3) return "";
        $signal_return_type = "";
        foreach ($words as $signal_word_index => $signal_word) {
            if ($signal_word_index >= count($words) - 1) continue;
            if ($signal_word === "virtual" || $signal_word === "abstract" || $signal_word === "override" || $signal_word === "signal") continue;
            $signal_return_type = $signal_word;
        }
        if ($signal_return_type !== "void") return "";
        $name = php_identifier_from_vala_name($words[count($words) - 1]);
        return "    /** @vala-signal " . $name . " */\n    public function on_" . $name . "(callable " . "$" . "callback): void {}\n\n";
    }
    if (count($words) < 2) {
        return "";
    }
    $name = php_identifier_from_vala_name($words[count($words) - 1]);
    $return_type = php_type($words[count($words) - 2], $php_namespace, $vala_namespace);
    return "    /** @vala-method " . $name . " */\n    public function " . $name . "(" . render_params($params, $php_namespace, $vala_namespace) . "): " . $return_type . " {}\n\n";
}

$options = parse_options($argv);
$input = require_option($options, "--input");
$output = require_option($options, "--output");
$vala_namespace = require_option($options, "--vala-namespace");
$source_namespace = optional_option($options, "--source-namespace");

$php_namespace = normalize_php_namespace(require_option($options, "--php-namespace"));
$pkg = require_option($options, "--pkg");

$contents = file_get_contents($input);
if ($contents === false) {
    fail("cannot read " . $input);
}

if (is_vapi_input($input)) {
    $stub = generate_php_stub_from_vapi($contents, $php_namespace, $source_namespace, $pkg);
    write_text_file($output, $stub);
    echo "Generated " . $output . " from " . $input . "\n";
    exit(0);
}

$stub = "<?php\n";
$stub .= "/**\n";
$stub .= " * Generated by php2vala-bindgen.\n";
$stub .= " * @php-namespace " . $php_namespace . "\n";
$stub .= " * @vala-namespace " . $vala_namespace . "\n";
$stub .= " * @pkg " . $pkg . "\n";
$stub .= " */\n";
$stub .= "namespace " . $php_namespace . ";\n\n";

$class_open = false;
$class_name = "";
$depth = 0;
$class_depth = 0;

foreach (split_text("\n", $contents) as $raw_line) {
    $line = trim($raw_line);
    if ($line === "" || str_starts_with($line, "[") || str_starts_with($line, "/*") || str_starts_with($line, "//")) {
        continue;
    }

    $handled_line = false;
    if (str_starts_with($line, "public ") && str_contains($line, " class ")) {
        if ($class_open) {
            $stub .= "}\n\n";
        }
        $rest = after_marker($line, " class ");
        $class_name = first_token($rest);
        $php_class_name = php_identifier_from_vala_name($class_name);
        $parent = "";
        $colon = strpos($rest, ":");
        $brace = strpos($rest, "{");
        if ($colon !== false && ($brace === false || $colon < $brace)) {
            $parent_part = substr($rest, $colon + 1);
            if ($brace !== false) {
                $parent_part = substr($parent_part, 0, strpos($parent_part, "{"));
            }
            $parent_bits = split_text(",", $parent_part);
            $parent = trim($parent_bits[0]);
        }

        $php_class = "\\" . $php_namespace . "\\" . $php_class_name;
        $vala_class = $vala_namespace . "." . $class_name;
        $stub .= "/**\n";
        $stub .= " * @php " . $php_class . "\n";
        $stub .= " * @vala " . $vala_class . "\n";
        $stub .= " * @pkg " . $pkg . "\n";
        $stub .= " */\n";
        $stub .= "class " . $php_class_name;
        if ($parent !== "") {
            $parent = str_replace($vala_namespace . ".", "", $parent);
            if (!str_contains($parent, ".")) {
                $stub .= " extends " . $parent;
            }
        }
        $stub .= " {\n";
        $class_open = true;
        $class_depth = $depth + 1;
        $handled_line = true;
    }

    if (!$handled_line && $class_open && $depth >= $class_depth && str_starts_with($line, "public ")) {
        $handled_member = false;
        if (str_contains($line, " signal ")) {
            $stub .= render_callable($line, "signal", $class_name, $php_namespace, $vala_namespace);
            $handled_member = true;
        }
        if (!$handled_member && str_contains($line, "(") && str_contains($line, ")")) {
            $before_paren = trim(substr($line, 0, strpos($line, "(")));
            $is_ctor = str_ends_with($before_paren, " " . $class_name) || str_ends_with($before_paren, "." . $class_name);
            $stub .= render_callable($line, $is_ctor ? "constructor" : "method", $class_name, $php_namespace, $vala_namespace);
            $handled_member = true;
        }
        if (!$handled_member && (str_contains($line, "{ get;") || str_contains($line, "{ owned get;"))) {
            $before = trim(substr($line, 0, strpos($line, "{")));
            $words = array();
            foreach (split_text(" ", $before) as $raw_property_word) {
                $property_word = trim($raw_property_word);
                if ($property_word !== "" && $property_word !== "public") {
                    $words[] = $property_word;
                }
            }
            if (count($words) >= 2) {
                $prop_name = $words[count($words) - 1];
                $prop_type = php_type($words[count($words) - 2], $php_namespace, $vala_namespace);
                $stub .= "    /** @vala-property " . $prop_name . " */\n";
                $stub .= "    public " . $prop_type . " $" . php_param_name($prop_name) . ";\n\n";
            }
        }
    }

    for ($i = 0; $i < strlen($line); $i++) {
        $char = substr($line, $i, 1);
        if ($char === "{") {
            $depth++;
        }
        if ($char === "}") {
            $depth--;
        }
    }
    if ($class_open && $depth < $class_depth) {
        $stub .= "}\n\n";
        $class_open = false;
        $class_depth = 0;
    }
}

if ($class_open) {
    $stub .= "}\n\n";
}

write_text_file($output, $stub);
echo "Generated " . $output . " from " . $input . "\n";
