using Gee;

public class PhpToValaSourceParser : Object {
    public static ResolvedSource resolve_compile_time_includes (string source_path, string source) throws Error {
        var include_stack = new HashMap<string, bool> ();
        return resolve_compile_time_includes_inner (source_path, source, include_stack);
    }

    private static ResolvedSource resolve_compile_time_includes_inner (string source_path, string source, HashMap<string, bool> include_stack) throws Error {
        string normalized_source_path = normalize_source_path (source_path);
        if (include_stack.has_key (normalized_source_path)) {
            throw new PhpToValaError.UNSUPPORTED (
                "Recursive compile-time include detected for %s",
                source_path
            );
        }

        include_stack[normalized_source_path] = true;
        var resolved = new ResolvedSource ();
        var output = new StringBuilder ();
        string[] lines = source.split ("\n");
        string source_dir = Path.get_dirname (source_path);

        for (int i = 0; i < lines.length; i++) {
            string raw_line = lines[i];
            string line = raw_line.strip ();
            string? include_path = parse_compile_time_include_path (line);

            if (include_path == null) {
                append_resolved_line (resolved, output, raw_line, source_path, i + 1);
                continue;
            }

            string resolved_path = Path.is_absolute (include_path)
                ? include_path
                : Path.build_filename (source_dir, include_path);

            string included_source;
            try {
                FileUtils.get_contents (resolved_path, out included_source);
            } catch (Error e) {
                throw new PhpToValaError.UNSUPPORTED (
                    "Compile-time include failed at %s:%d: cannot read %s: %s",
                    source_path,
                    i + 1,
                    resolved_path,
                    e.message
                );
            }

            append_resolved_line (resolved, output, "/* php2vala compile-time include: %s */".printf (resolved_path), source_path, i + 1);
            ResolvedSource included = resolve_compile_time_includes_inner (resolved_path, included_source, include_stack);
            append_resolved_source (resolved, output, included);
            append_resolved_line (resolved, output, "/* php2vala end compile-time include: %s */".printf (resolved_path), source_path, i + 1);
        }

        include_stack.unset (normalized_source_path);
        resolved.source = output.str;
        return resolved;
    }

    private static void append_resolved_line (ResolvedSource resolved, StringBuilder output, string line, string source_path, int source_line) {
        output.append (line);
        output.append ("\n");
        resolved.files.add (source_path);
        resolved.lines.add (source_line);
    }

    private static void append_resolved_source (ResolvedSource resolved, StringBuilder output, ResolvedSource included) {
        output.append (included.source);
        foreach (string file in included.files) {
            resolved.files.add (file);
        }
        foreach (int line in included.lines) {
            resolved.lines.add (line);
        }
    }

    private static string normalize_source_path (string source_path) {
        if (Path.is_absolute (source_path)) {
            return source_path;
        }
        return Path.build_filename (Environment.get_current_dir (), source_path);
    }

    private static string? parse_compile_time_include_path (string line) throws Error {
        string? expression = null;
        if (line.has_prefix ("include_once")) {
            expression = line.substring ("include_once".length).strip ();
        } else if (line.has_prefix ("require_once")) {
            expression = line.substring ("require_once".length).strip ();
        } else if (line.has_prefix ("include")) {
            expression = line.substring ("include".length).strip ();
        } else if (line.has_prefix ("require")) {
            expression = line.substring ("require".length).strip ();
        }

        if (expression == null) {
            return null;
        }

        if (!expression.has_suffix (";")) {
            throw new PhpToValaError.UNSUPPORTED (
                "Only statement-style compile-time include/require is supported: %s",
                line
            );
        }

        expression = expression.substring (0, expression.length - 1).strip ();
        if (expression.has_prefix ("(") && expression.has_suffix (")")) {
            expression = expression.substring (1, expression.length - 2).strip ();
        }

        if ((expression.has_prefix ("\"") && expression.has_suffix ("\""))
            || (expression.has_prefix ("'") && expression.has_suffix ("'"))) {
            return expression.substring (1, expression.length - 2);
        }

        throw new PhpToValaError.UNSUPPORTED (
            "Only string-literal compile-time include/require is supported: %s",
            line
        );
    }

    public static string strip_php_tags (string source) {
        var output = new StringBuilder ();
        string[] lines = source.split ("\n");
        for (int i = 0; i < lines.length; i++) {
            string line = lines[i];
            string stripped = line.strip ();
            if (stripped.has_prefix ("<?php")) {
                int index = line.index_of ("<?php");
                line = line.substring (0, index) + line.substring (index + 5);
            } else if (stripped.has_prefix ("<?")) {
                int index = line.index_of ("<?");
                line = line.substring (0, index) + line.substring (index + 2);
            }
            string stripped_line = line.strip ();
            if (stripped_line == "?>") {
                line = "";
            } else if (stripped_line.has_suffix ("?>") && !line.contains ("\"?>\"") && !line.contains ("'?>'")) {
                int index = line.last_index_of ("?>");
                if (index >= 0) {
                    line = line.substring (0, index) + line.substring (index + 2);
                }
            }
            output.append (line);
            if (i + 1 < lines.length) {
                output.append_c ('\n');
            }
        }
        return output.str;
    }

    public static SourceParts extract_local_functions (string source_path, string source) throws Error {
        return extract_local_functions_with_php_tokens (source_path, source);
    }

    public static string debug_tokens_for_source_line (string source_path, string source, int php_line_number, int context_lines = 1) {
        try {
            var tokens = tokenize_php_source (source_path, source);
            return format_token_debug_context (tokens, php_line_number, context_lines);
        } catch (Error e) {
            return "PHP tokens near line %d: <tokenize failed: %s>".printf (php_line_number, e.message);
        }
    }

    public static string debug_tokens_for_snippet (string source_path, string line, int php_line_number) {
        try {
            var tokens = tokenize_php_source (source_path, line);
            return format_token_debug_context (tokens, php_line_number, 0);
        } catch (Error e) {
            return "PHP tokens near line %d: <tokenize failed: %s>".printf (php_line_number, e.message);
        }
    }

    private class PhpSourceToken : Object {
        public string name;
        public string text;
        public int line;

        public PhpSourceToken (string name, string text, int line) {
            this.name = name;
            this.text = text;
            this.line = line;
        }

        public bool is_symbol (string value) {
            return name == value;
        }

        public bool is_token (string value) {
            return name == value;
        }
    }

    private class SourceSpan : Object {
        public int start_line;
        public int end_line;

        public SourceSpan (int start_line, int end_line) {
            this.start_line = start_line;
            this.end_line = end_line;
        }
    }

    private static SourceParts extract_local_functions_with_php_tokens (string source_path, string source) throws Error {
        var parts = new SourceParts ();
        var tokens = tokenize_php_source (source_path, source);
        var extracted_spans = new ArrayList<SourceSpan> ();

        string pending_doc_comment = "";
        int pending_doc_line = 0;
        int brace_depth = 0;

        for (int i = 0; i < tokens.size; i++) {
            PhpSourceToken token = tokens[i];

            if (brace_depth == 0 && token.is_token ("T_DOC_COMMENT")) {
                pending_doc_comment = token.text;
                pending_doc_line = token.line;
                continue;
            }

            if (brace_depth == 0 && is_class_token (tokens, i)) {
                int class_end_index;
                int class_start_line;
                int class_end_line;
                PhpClass? php_class = parse_php_class_from_tokens (source_path, tokens, i, pending_doc_comment, out class_end_index, out class_start_line, out class_end_line);
                if (php_class != null) {
                    parts.classes.add (php_class);
                    extracted_spans.add (new SourceSpan (pending_doc_line > 0 ? pending_doc_line : class_start_line, class_end_line));
                    pending_doc_comment = "";
                    pending_doc_line = 0;
                    i = class_end_index;
                    continue;
                }
            }

            if (brace_depth == 0 && token.is_token ("T_FUNCTION")) {
                int function_end_index;
                int function_start_line;
                int function_end_line;
                LocalFunction? local_function = parse_local_function_from_tokens (source_path, tokens, i, tokens.size, true, pending_doc_comment, out function_end_index, out function_start_line, out function_end_line);
                if (local_function != null) {
                    parts.functions.add (local_function);
                    extracted_spans.add (new SourceSpan (pending_doc_line > 0 ? pending_doc_line : function_start_line, function_end_line));
                    pending_doc_comment = "";
                    pending_doc_line = 0;
                    i = function_end_index;
                    continue;
                }
            }

            if (token.is_symbol ("{")) {
                brace_depth++;
                pending_doc_comment = "";
                pending_doc_line = 0;
            } else if (token.is_symbol ("}")) {
                brace_depth--;
                if (brace_depth < 0) {
                    brace_depth = 0;
                }
                pending_doc_comment = "";
                pending_doc_line = 0;
            } else if (!is_ignorable_token (token)) {
                pending_doc_comment = "";
                pending_doc_line = 0;
            }
        }

        parts.main_source = build_main_source_without_spans (source, extracted_spans);
        return parts;
    }

    private static ArrayList<PhpSourceToken> tokenize_php_source (string source_path, string source) throws Error {
        int line_offset = 0;
        string php_source = source;
        string leading = php_source.strip ();
        if (!leading.has_prefix ("<?php") && !leading.has_prefix ("<?")) {
            php_source = "<?php\n" + php_source;
            line_offset = 1;
        }

        Php.zval token_list = PhpEmbed.token_get_all_zval (
            source_path,
            1,
            Php.zval.string_value (php_source),
            Php.zval.boolean (true)
        );
        var tokens = new ArrayList<PhpSourceToken> ();
        int current_line = 1;

        foreach (Php.zval token_value in Php.array_values (token_list)) {
            string token_name;
            string token_text;
            int token_line;

            if (token_value.get_value_type () == Php.ValueType.STRING) {
                token_name = token_value.as_string ();
                token_text = token_name;
                token_line = current_line;
            } else {
                Php.zval token_id_value = Php.array_get (token_value, Php.zval.integer (0));
                Php.zval token_text_value = Php.array_get (token_value, Php.zval.integer (1));
                Php.zval token_line_value = Php.array_get (token_value, Php.zval.integer (2));
                int token_id = Php.zval_to_int (token_id_value);

                token_name = PhpEmbed.token_name_static (token_id, source_path, Php.zval_to_int (token_line_value));
                token_text = Php.zval_to_string (token_text_value);
                token_line = Php.zval_to_int (token_line_value);
            }

            int output_line = token_line - line_offset;
            if (output_line < 1) {
                output_line = 1;
            }
            tokens.add (new PhpSourceToken (token_name, token_text, output_line));
            current_line = token_line + count_newlines (token_text);
        }

        return tokens;
    }

    private static string format_token_debug_context (ArrayList<PhpSourceToken> tokens, int php_line_number, int context_lines) {
        int first_line = int.max (1, php_line_number - context_lines);
        int last_line = php_line_number + context_lines;
        var output = new StringBuilder ();
        output.append_printf ("PHP tokens near line %d:", php_line_number);
        bool found = false;

        foreach (PhpSourceToken token in tokens) {
            if (token.line < first_line || token.line > last_line) {
                continue;
            }
            if (token.is_token ("T_WHITESPACE")) {
                continue;
            }
            found = true;
            output.append_printf (
                "\n  %d: %s %s",
                token.line,
                token.name,
                quote_debug_token_text (token.text)
            );
        }

        if (!found) {
            output.append (" <no non-whitespace tokens>");
        }
        return output.str;
    }

    private static string quote_debug_token_text (string text) {
        string escaped = text
            .replace ("\\", "\\\\")
            .replace ("\n", "\\n")
            .replace ("\r", "\\r")
            .replace ("\t", "\\t")
            .replace ("\"", "\\\"");
        if (escaped.length > 80) {
            escaped = escaped.substring (0, 77) + "...";
        }
        return "\"%s\"".printf (escaped);
    }

    private static bool is_class_token (ArrayList<PhpSourceToken> tokens, int index) {
        PhpSourceToken token = tokens[index];
        if (token.is_token ("T_CLASS")) {
            return true;
        }
        if (!token.is_token ("T_ABSTRACT")) {
            return false;
        }
        int next = next_significant_token_index (tokens, index + 1, tokens.size);
        return next >= 0 && tokens[next].is_token ("T_CLASS");
    }

    private static PhpClass? parse_php_class_from_tokens (string source_path, ArrayList<PhpSourceToken> tokens, int class_token_index, string doc_comment, out int class_end_index, out int class_start_line, out int class_end_line) throws Error {
        class_end_index = class_token_index;
        class_start_line = tokens[class_token_index].line;
        class_end_line = class_start_line;

        int class_index = tokens[class_token_index].is_token ("T_CLASS")
            ? class_token_index
            : next_significant_token_index (tokens, class_token_index + 1, tokens.size);
        if (class_index < 0 || !tokens[class_index].is_token ("T_CLASS")) {
            return null;
        }

        int name_index = next_significant_token_index (tokens, class_index + 1, tokens.size);
        if (name_index < 0 || !tokens[name_index].is_token ("T_STRING")) {
            return null;
        }

        string class_name = tokens[name_index].text;
        string extends_name = "";
        int open_brace_index = -1;
        for (int i = name_index + 1; i < tokens.size; i++) {
            if (tokens[i].is_symbol ("{")) {
                open_brace_index = i;
                break;
            }
            if (tokens[i].is_token ("T_EXTENDS")) {
                int extends_index = next_significant_token_index (tokens, i + 1, tokens.size);
                if (extends_index >= 0) {
                    extends_name = collect_name_tokens (tokens, extends_index);
                }
            }
        }
        if (open_brace_index < 0) {
            return null;
        }

        int close_brace_index = find_matching_brace_token (tokens, open_brace_index, tokens.size);
        if (close_brace_index < 0) {
            throw new PhpToValaError.UNSUPPORTED (
                "Unclosed PHP class '%s' at %s:%d",
                class_name,
                source_path,
                tokens[class_index].line
            );
        }

        var php_class = new PhpClass (class_name, tokens[class_index].line, doc_comment, extends_name);
        parse_class_methods_from_tokens (source_path, tokens, open_brace_index + 1, close_brace_index, php_class);

        class_end_index = close_brace_index;
        class_start_line = tokens[class_token_index].line;
        class_end_line = tokens[close_brace_index].line;
        return php_class;
    }

    private static void parse_class_methods_from_tokens (string source_path, ArrayList<PhpSourceToken> tokens, int start_index, int end_index, PhpClass php_class) throws Error {
        int depth = 1;
        string pending_doc_comment = "";

        for (int i = start_index; i < end_index; i++) {
            PhpSourceToken token = tokens[i];
            if (depth == 1 && token.is_token ("T_DOC_COMMENT")) {
                pending_doc_comment = token.text;
                continue;
            }
            if (depth == 1 && token.is_token ("T_FUNCTION")) {
                bool is_static = previous_method_modifiers_contain_static (tokens, start_index, i);
                int method_end_index;
                int method_start_line;
                int method_end_line;
                LocalFunction? method = parse_local_function_from_tokens (source_path, tokens, i, end_index, is_static, pending_doc_comment, out method_end_index, out method_start_line, out method_end_line);
                if (method != null) {
                    php_class.methods.add (method);
                    pending_doc_comment = "";
                    i = method_end_index;
                    continue;
                }
            }
            if (token.is_symbol ("{")) {
                depth++;
                pending_doc_comment = "";
            } else if (token.is_symbol ("}")) {
                depth--;
                pending_doc_comment = "";
            } else if (!is_ignorable_token (token)) {
                pending_doc_comment = "";
            }
        }
    }

    private static LocalFunction? parse_local_function_from_tokens (string source_path, ArrayList<PhpSourceToken> tokens, int function_index, int end_limit, bool is_static, string doc_comment, out int function_end_index, out int function_start_line, out int function_end_line) throws Error {
        function_end_index = function_index;
        function_start_line = tokens[function_index].line;
        function_end_line = function_start_line;

        int name_index = next_significant_token_index (tokens, function_index + 1, end_limit);
        if (name_index >= 0 && tokens[name_index].text == "&") {
            name_index = next_significant_token_index (tokens, name_index + 1, end_limit);
        }
        if (name_index < 0 || !tokens[name_index].is_token ("T_STRING")) {
            return null;
        }

        int open_paren_index = next_symbol_token_index (tokens, name_index + 1, end_limit, "(");
        if (open_paren_index < 0) {
            return null;
        }
        int close_paren_index = find_matching_symbol_token (tokens, open_paren_index, end_limit, "(", ")");
        if (close_paren_index < 0) {
            return null;
        }

        int open_brace_index = next_symbol_token_index (tokens, close_paren_index + 1, end_limit, "{");
        if (open_brace_index < 0) {
            return null;
        }
        int close_brace_index = find_matching_brace_token (tokens, open_brace_index, end_limit);
        if (close_brace_index < 0) {
            throw new PhpToValaError.UNSUPPORTED (
                "Unclosed PHP function '%s' at %s:%d",
                tokens[name_index].text,
                source_path,
                tokens[function_index].line
            );
        }

        string parameters = tokens_to_compact_source (tokens, open_paren_index + 1, close_paren_index);
        string return_type = tokens_to_compact_source (tokens, close_paren_index + 1, open_brace_index).strip ();
        string header = "function %s(%s) %s {".printf (tokens[name_index].text, parameters, return_type);

        string function_name;
        string? function_return_type;
        var function_parameters = new ArrayList<LocalParameter> ();
        parse_local_function_header (source_path, header, tokens[function_index].line, out function_name, function_parameters, out function_return_type);

        string body = tokens_to_source (tokens, open_brace_index + 1, close_brace_index, tokens[function_index].line + 1);
        function_end_index = close_brace_index;
        function_start_line = tokens[function_index].line;
        function_end_line = tokens[close_brace_index].line;

        return new LocalFunction (
            function_name,
            copy_parameter_list (function_parameters),
            function_return_type,
            body,
            tokens[function_index].line,
            is_static,
            doc_comment
        );
    }

    private static int next_significant_token_index (ArrayList<PhpSourceToken> tokens, int start_index, int end_limit) {
        for (int i = start_index; i < end_limit; i++) {
            if (!is_ignorable_token (tokens[i])) {
                return i;
            }
        }
        return -1;
    }

    private static int next_symbol_token_index (ArrayList<PhpSourceToken> tokens, int start_index, int end_limit, string symbol) {
        for (int i = start_index; i < end_limit; i++) {
            if (tokens[i].is_symbol (symbol)) {
                return i;
            }
        }
        return -1;
    }

    private static int find_matching_brace_token (ArrayList<PhpSourceToken> tokens, int open_index, int end_limit) {
        return find_matching_symbol_token (tokens, open_index, end_limit, "{", "}");
    }

    private static int find_matching_symbol_token (ArrayList<PhpSourceToken> tokens, int open_index, int end_limit, string open_symbol, string close_symbol) {
        int depth = 0;
        for (int i = open_index; i < end_limit; i++) {
            if (tokens[i].is_symbol (open_symbol)) {
                depth++;
            } else if (tokens[i].is_symbol (close_symbol)) {
                depth--;
                if (depth == 0) {
                    return i;
                }
            }
        }
        return -1;
    }

    private static bool previous_method_modifiers_contain_static (ArrayList<PhpSourceToken> tokens, int class_start_index, int function_index) {
        for (int i = function_index - 1; i >= class_start_index; i--) {
            PhpSourceToken token = tokens[i];
            if (token.is_token ("T_STATIC")) {
                return true;
            }
            if (token.is_symbol (";") || token.is_symbol ("}") || token.is_symbol ("{")) {
                return false;
            }
        }
        return false;
    }

    private static bool is_ignorable_token (PhpSourceToken token) {
        return token.is_token ("T_WHITESPACE")
            || token.is_token ("T_COMMENT")
            || token.is_token ("T_OPEN_TAG")
            || token.is_token ("T_CLOSE_TAG");
    }

    private static string collect_name_tokens (ArrayList<PhpSourceToken> tokens, int start_index) {
        var output = new StringBuilder ();
        for (int i = start_index; i < tokens.size; i++) {
            PhpSourceToken token = tokens[i];
            if (token.is_token ("T_STRING") || token.is_token ("T_NAME_QUALIFIED") || token.is_token ("T_NS_SEPARATOR")) {
                output.append (token.text);
                continue;
            }
            break;
        }
        return output.str;
    }

    private static string tokens_to_compact_source (ArrayList<PhpSourceToken> tokens, int start_index, int end_index) {
        var output = new StringBuilder ();
        for (int i = start_index; i < end_index; i++) {
            output.append (tokens[i].text);
        }
        return output.str;
    }

    private static string tokens_to_source (ArrayList<PhpSourceToken> tokens, int start_index, int end_index, int first_line) {
        var output = new StringBuilder ();
        int current_line = first_line;
        for (int i = start_index; i < end_index; i++) {
            PhpSourceToken token = tokens[i];
            while (current_line < token.line) {
                output.append ("\n");
                current_line++;
            }
            output.append (token.text);
            current_line += count_newlines (token.text);
        }
        return output.str;
    }

    private static int count_newlines (string text) {
        int count = 0;
        for (int i = 0; i < text.length; i++) {
            if (text[i] == '\n') {
                count++;
            }
        }
        return count;
    }

    private static string build_main_source_without_spans (string source, ArrayList<SourceSpan> spans) {
        var output = new StringBuilder ();
        string[] lines = source.split ("\n");

        for (int i = 0; i < lines.length; i++) {
            int line_number = i + 1;
            if (!line_is_inside_span (line_number, spans)) {
                output.append (lines[i]);
            }
            output.append ("\n");
        }

        return output.str;
    }

    private static bool line_is_inside_span (int line_number, ArrayList<SourceSpan> spans) {
        foreach (SourceSpan span in spans) {
            if (line_number >= span.start_line && line_number <= span.end_line) {
                return true;
            }
        }
        return false;
    }

    public static Statements collect_statements (string body, int first_line_number = 1) {
        var statements = new Statements ();
        string[] lines = body.split ("\n");
        StringBuilder? pending = null;
        int pending_line = 0;
        StringBuilder? pending_comment = null;
        int pending_comment_line = 0;
        int php_line_number = first_line_number - 1;

        foreach (string raw_line in lines) {
            php_line_number++;
            string line = raw_line.strip ();

            if (pending_comment != null) {
                pending_comment.append ("\n");
                pending_comment.append (line);

                if (line.index_of ("*/") >= 0) {
                    statements.texts.add (pending_comment.str);
                    statements.lines.add (pending_comment_line);
                    statements.columns.add (1);
                    pending_comment = null;
                }
                continue;
            }

            if (line == "" && pending == null) {
                continue;
            }

            if (line.has_prefix ("/*")) {
                if (line.index_of ("*/") >= 0) {
                    statements.texts.add (line);
                    statements.lines.add (php_line_number);
                    statements.columns.add (first_non_whitespace_column (raw_line));
                } else {
                    pending_comment = new StringBuilder (line);
                    pending_comment_line = php_line_number;
                }
                continue;
            }

            if (pending != null) {
                if (line.has_prefix ("//") || line.has_prefix ("#")) {
                    continue;
                }
                line = strip_line_comment (line).strip ();
                if (line == "") {
                    continue;
                }
                pending.append (" ");
                pending.append (line);

                if (line.has_suffix (";") && nesting_depth (pending.str) <= 0) {
                    statements.texts.add (pending.str);
                    statements.lines.add (pending_line);
                    statements.columns.add (1);
                    pending = null;
                }
                continue;
            }

            if (starts_multiline_assignment (line) || starts_multiline_parenthesized_statement (line)) {
                pending = new StringBuilder (line);
                pending_line = php_line_number;
                continue;
            }

            add_line_statements (statements, line, php_line_number);
        }

        if (pending != null) {
            statements.texts.add (pending.str);
            statements.lines.add (pending_line);
            statements.columns.add (1);
        }
        if (pending_comment != null) {
            statements.texts.add (pending_comment.str);
            statements.lines.add (pending_comment_line);
            statements.columns.add (1);
        }

        return statements;
    }

    private static void add_line_statements (Statements statements, string line, int php_line_number) {
        foreach (StatementSlice slice in split_statement_line (line)) {
            string clean = slice.text.strip ();
            if (clean == "") {
                continue;
            }
            statements.texts.add (clean);
            statements.lines.add (php_line_number);
            statements.columns.add (slice.column);
        }
    }

    private class StatementSlice : Object {
        public string text;
        public int column;

        public StatementSlice (string text, int column) {
            this.text = text;
            this.column = column;
        }
    }

    private static ArrayList<StatementSlice> split_statement_line (string line) {
        var parts = new ArrayList<StatementSlice> ();
        int depth = 0;
        bool in_single_quote = false;
        bool in_double_quote = false;
        int start = 0;

        for (int i = 0; i < line.length; i++) {
            char c = line[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '/' && i + 1 < line.length && line[i + 1] == '/') {
                    if (start < line.length) {
                        string text = line.substring (start);
                        parts.add (new StatementSlice (text, start + first_non_whitespace_column (text)));
                    }
                    return parts;
                }
                if (c == '#') {
                    if (start < line.length) {
                        string text = line.substring (start);
                        parts.add (new StatementSlice (text, start + first_non_whitespace_column (text)));
                    }
                    return parts;
                }
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (c == ';' && depth == 0) {
                    string text = line.substring (start, i - start + 1);
                    parts.add (new StatementSlice (text, start + first_non_whitespace_column (text)));
                    start = i + 1;
                }
            }
        }

        if (start < line.length) {
            string text = line.substring (start);
            parts.add (new StatementSlice (text, start + first_non_whitespace_column (text)));
        }
        return parts;
    }

    private static string strip_line_comment (string line) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        bool escaped = false;

        for (int i = 0; i < line.length; i++) {
            char c = line[i];
            char next = i + 1 < line.length ? line[i + 1] : '\0';

            if (escaped) {
                escaped = false;
                continue;
            }
            if ((in_single_quote || in_double_quote) && c == '\\') {
                escaped = true;
                continue;
            }
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
                continue;
            }
            if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
                continue;
            }
            if (!in_single_quote && !in_double_quote) {
                if (c == '/' && next == '/') {
                    return line.substring (0, i);
                }
                if (c == '#') {
                    return line.substring (0, i);
                }
            }
        }

        return line;
    }

    private static int first_non_whitespace_column (string line) {
        for (int i = 0; i < line.length; i++) {
            if (line[i] != ' ' && line[i] != '\t') {
                return i + 1;
            }
        }
        return 1;
    }

    private static void parse_local_function_header (string source_path, string line, int php_line_number, out string function_name, ArrayList<LocalParameter> parameters, out string? return_type) throws Error {
        parameters.clear ();
        return_type = null;
        string header = line;
        int brace_index = header.index_of ("{");
        if (brace_index >= 0) {
            header = header.substring (0, brace_index).strip ();
        }

        string rest = header.substring ("function ".length).strip ();
        int open_index = rest.index_of ("(");
        int close_index = rest.last_index_of (")");
        if (open_index <= 0 || close_index < open_index) {
            throw unsupported_syntax (source_path, php_line_number, line);
        }

        function_name = rest.substring (0, open_index).strip ();
        if (!is_identifier (function_name)) {
            throw unsupported_syntax (source_path, php_line_number, line);
        }

        string raw_parameters = rest.substring (open_index + 1, close_index - open_index - 1).strip ();
        string raw_return_type = rest.substring (close_index + 1).strip ();
        if (raw_return_type.has_prefix (":") ) {
            return_type = raw_return_type.substring (1).strip ();
            if (return_type == "") {
                return_type = null;
            }
        }
        if (raw_parameters == "") {
            return;
        }

        foreach (string raw_parameter in split_top_level (raw_parameters, ',')) {
            string parameter = raw_parameter.strip ();
            string? default_value = null;
            int equals_index = parameter.index_of ("=");
            if (equals_index >= 0) {
                default_value = parameter.substring (equals_index + 1).strip ();
                parameter = parameter.substring (0, equals_index).strip ();
            }
            string parameter_name = extract_parameter_name (parameter);
            if (!is_identifier (parameter_name)) {
                throw unsupported_syntax (source_path, php_line_number, line);
            }
            string? type_name = extract_parameter_type (parameter);
            parameters.add (new LocalParameter (parameter_name, type_name, default_value));
        }
    }

    private static string? extract_parameter_type (string parameter) {
        string clean = parameter.strip ();
        int dollar_index = clean.index_of ("$");
        if (dollar_index <= 0) {
            return null;
        }
        string type_name = clean.substring (0, dollar_index).strip ();
        if (type_name == "" || type_name == "&" || type_name == "...") {
            return null;
        }
        return type_name;
    }

    private static string extract_parameter_name (string parameter) {
        string clean = parameter.strip ();

        int dollar_index = clean.index_of ("$");
        if (dollar_index >= 0) {
            clean = clean.substring (dollar_index + 1);
        }

        if (clean.has_prefix ("&")) {
            clean = clean.substring (1).strip ();
        }
        if (clean.has_prefix ("...")) {
            clean = clean.substring (3).strip ();
        }

        int end = 0;
        while (end < clean.length && (clean[end].isalnum () || clean[end] == '_')) {
            end++;
        }

        return end > 0 ? clean.substring (0, end) : clean;
    }

    private static ArrayList<LocalParameter> copy_parameter_list (ArrayList<LocalParameter> input) {
        var result = new ArrayList<LocalParameter> ();
        foreach (LocalParameter value in input) {
            result.add (new LocalParameter (value.name, value.type_name, value.default_value));
        }
        return result;
    }

    private static bool starts_multiline_assignment (string line) {
        return line.has_prefix ("$")
            && line.index_of ("=") > 0
            && !line.has_suffix (";")
            && (line.index_of ("'") >= 0
                || line.index_of ("\"") >= 0
                || line.index_of ("[") >= 0
                || line.index_of ("array(") >= 0);
    }

    private static bool starts_multiline_parenthesized_statement (string line) {
        if (line.has_suffix (";") || line.has_suffix ("{") || line == "}" || line.has_prefix ("//") || line.has_prefix ("#")) {
            return false;
        }
        return nesting_depth (strip_line_comment (line)) > 0;
    }

    private static int nesting_depth (string line) {
        int depth = 0;
        bool in_single_quote = false;
        bool in_double_quote = false;
        bool escaped = false;

        for (int i = 0; i < line.length; i++) {
            char c = line[i];
            if (escaped) {
                escaped = false;
                continue;
            }
            if ((in_single_quote || in_double_quote) && c == '\\') {
                escaped = true;
                continue;
            }
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
                continue;
            }
            if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
                continue;
            }
            if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[') {
                    depth++;
                } else if (c == ')' || c == ']') {
                    depth--;
                }
            }
        }

        return depth;
    }

    private static bool is_identifier (string name) {
        if (name.length == 0) {
            return false;
        }
        for (int i = 0; i < name.length; i++) {
            char c = name[i];
            bool valid = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || (i > 0 && c >= '0' && c <= '9');
            if (!valid) {
                return false;
            }
        }
        return true;
    }

    private static ArrayList<string> split_top_level (string expression, char separator) {
        var parts = new ArrayList<string> ();
        int depth = 0;
        bool in_single_quote = false;
        bool in_double_quote = false;
        int start = 0;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[') {
                    depth++;
                } else if (c == ')' || c == ']') {
                    depth--;
                } else if (c == separator && depth == 0) {
                    parts.add (expression.substring (start, i - start));
                    start = i + 1;
                }
            }
        }

        parts.add (expression.substring (start));
        return parts;
    }

    private static Error unsupported_syntax (string source_path, int php_line_number, string line) {
        return new PhpToValaError.UNSUPPORTED (
            "Unsupported PHP syntax at %s:%d: %s\n%s",
            source_path,
            php_line_number,
            line,
            debug_tokens_for_snippet (source_path, line, php_line_number)
        );
    }
}
