<?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 detect_vapi_namespace($contents): string {
    foreach (split_text("\n", $contents) as $raw_line) {
        $line = trim($raw_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);
    foreach (array("owned ", "unowned ", "weak ") as $prefix) {
        if (str_starts_with($type, $prefix)) {
            $type = trim(substr($type, strlen($prefix)));
        }
    }
    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 php_type_from_vapi($source_type, $php_namespace, $source_namespace): string {
    $type = normalize_vapi_type($source_type);
    if ($type === "void") return "void";
    if ($type === "string?") return "?string";
    if ($type === "string") return "string";
    if ($type === "int" || $type === "uint" || $type === "int32" || $type === "uint32" || $type === "int64" || $type === "uint64" || $type === "size_t") return "int";
    if ($type === "bool") return "bool";
    if ($type === "double" || $type === "float") return "float";
    if (str_ends_with($type, "[]")) return "array";
    if (str_contains($type, ".")) return "object";
    return "int";
}

function render_vapi_params_for_php($raw_params, $php_namespace, $source_namespace): string {
    $result = array();
    $raw_params = trim($raw_params);
    if ($raw_params === "") {
        return "";
    }
    foreach (split_text(",", $raw_params) as $raw_param) {
        $param_part = trim($raw_param);
        if ($param_part === "" || str_starts_with($param_part, "out ") || str_starts_with($param_part, "ref ")) {
            return "";
        }
        if (str_contains($param_part, "*") || str_contains($param_part, "[]")) {
            return "";
        }
        $default_text = "";
        $eq = strpos($param_part, "=");
        if ($eq !== false) {
            $default_text = vapi_default_for_php(substr($param_part, $eq + 1));
            $param_part = trim(substr($param_part, 0, $eq));
        }

        $pieces = array();
        foreach (split_text(" ", $param_part) as $raw_param_piece) {
            $trimmed_param_piece = trim($raw_param_piece);
            if ($trimmed_param_piece !== "") {
                $pieces[] = $trimmed_param_piece;
            }
        }
        if (count($pieces) < 2) {
            continue;
        }

        $name = php_param_name($pieces[count($pieces) - 1]);
        $type_parts = array();
        foreach ($pieces as $param_piece_index => $type_param_piece) {
            if ($param_piece_index < count($pieces) - 1) {
                $type_parts[] = $type_param_piece;
            }
        }
        $result[] = php_type_from_vapi(implode(" ", $type_parts), $php_namespace, $source_namespace) . " $" . $name . $default_text;
    }
    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 (str_contains($line, " signal ") || str_contains($line, " static ")) {
        return "";
    }
    if (str_contains($line, "void *") || str_contains($line, "void*")) {
        return "";
    }

    $line = substr($line, 0, strlen($line) - 1);
    $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);
    $before = trim(substr($before, strlen("public ")));

    if ($kind === "constructor") {
        return "    /**\n     * @vala-constructor __construct\n     */\n    public function __construct(" . render_vapi_params_for_php($params, $php_namespace, $source_namespace) . ") {}\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 "";
    }
    $name = $words[count($words) - 1];
    $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);
    return "    /**\n     * @vala-method " . $name . "\n     */\n    public function " . $name . "(" . render_vapi_params_for_php($params, $php_namespace, $source_namespace) . "): " . $return_type . " {}\n\n";
}

function is_identifier_text($text): bool {
    $text = trim($text);
    if ($text === "") return false;
    $first = substr($text, 0, 1);
    if (!(($first >= "A" && $first <= "Z") || ($first >= "a" && $first <= "z") || $first === "_")) return false;
    for ($i = 1; $i < strlen($text); $i++) {
        $char = substr($text, $i, 1);
        if (!(($char >= "A" && $char <= "Z") || ($char >= "a" && $char <= "z") || ($char >= "0" && $char <= "9") || $char === "_")) return false;
    }
    return true;
}

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 "";
    $stub = "class " . $class_name . " {\n";
    foreach ($names as $name) {
        if (is_identifier_text($name)) $stub .= "    public const " . $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("}", " ", 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, "=")) $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 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);

    $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, "//") || str_starts_with($line, "*")) {
            continue;
        }

        if (!$class_open && str_starts_with($line, "public class ")) {
            $rest = trim(substr($line, strlen("public class ")));
            $class_name = first_token($rest);
            $php_class = "\\" . $php_namespace . "\\" . $class_name;
            $vala_class = $source_namespace . "." . $class_name;
            $stub .= "/**\n";
            $stub .= " * @php " . $php_class . "\n";
            $stub .= " * @vala " . $vala_class . "\n";
            $stub .= " * @pkg " . $pkg . "\n";
            $stub .= " */\n";
            $stub .= "class " . $class_name . " {\n";
            $class_open = true;
            $class_depth = $depth + 1;
        } else if ($class_open && $depth >= $class_depth && str_starts_with($line, "public ")) {
            $before_paren = $line;
            if (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_vapi_callable_for_php($line, $is_ctor ? "constructor" : "method", $class_name, $php_namespace, $source_namespace);
        }

        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_name = "";
            $class_depth = 0;
        }
    }
    if ($class_open) {
        $stub .= "}\n\n";
    }
    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";

    $prefix = $vala_namespace . ".";
    if (str_starts_with($type, $prefix)) {
        $type = substr($type, strlen($prefix));
        return "\\" . $php_namespace . "\\" . str_replace(".", "\\", $type);
    }
    if (str_contains($type, ".")) return "object";
    return "\\" . $php_namespace . "\\" . $type;
}

function php_param_name($name): string {
    $name = str_replace("-", "_", $name);
    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 "";
        $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 = $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);
        $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 . "\\" . $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 " . $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";
