using Gee;

public class PhpToValaAstBuilder : Object {
    public static string debug_ast_dump (PhpAstProgram ast_program) {
        var output = new StringBuilder ();
        output.append ("# php2vala AST dump\n");
        output.append_printf ("functions\t%d\n", ast_program.functions.size);
        foreach (LocalFunction local_function in ast_program.functions) {
            output.append_printf (
                "function\t%d\t%s\tparams=%d\treturn=%s\tstatements=%d\n",
                local_function.line,
                local_function.name,
                local_function.parameters.size,
                local_function.return_type ?? "",
                ast_program.statements_for_function (local_function).size
            );
            output.append ("line\tcolumn\tkind\ttext\n");
            foreach (PhpAstStatement statement in ast_program.statements_for_function (local_function)) {
                output.append_printf (
                    "%d\t%d\t%s\t%s\n",
                    statement.line,
                    statement.column,
                    ast_statement_kind_name (statement.kind),
                    escape_debug_field (statement.text)
                );
            }
        }
        output.append_printf ("classes\t%d\n", ast_program.classes.size);
        foreach (PhpClass php_class in ast_program.classes) {
            output.append_printf (
                "class\t%d\t%s\textends=%s\tmethods=%d\n",
                php_class.line,
                php_class.name,
                php_class.extends_name,
                php_class.methods.size
            );
            foreach (LocalFunction method in php_class.methods) {
                output.append_printf (
                    "method\t%d\t%s::%s\tparams=%d\treturn=%s\tstatements=%d\n",
                    method.line,
                    php_class.name,
                    method.name,
                    method.parameters.size,
                    method.return_type ?? "",
                    ast_program.statements_for_method (php_class, method).size
                );
                output.append ("line\tcolumn\tkind\ttext\n");
                foreach (PhpAstStatement statement in ast_program.statements_for_method (php_class, method)) {
                    output.append_printf (
                        "%d\t%d\t%s\t%s\n",
                        statement.line,
                        statement.column,
                        ast_statement_kind_name (statement.kind),
                        escape_debug_field (statement.text)
                    );
                }
            }
        }
        output.append_printf ("main_statements\t%d\n", ast_program.main_statements.size);
        output.append ("line\tcolumn\tkind\ttext\n");
        foreach (PhpAstStatement statement in ast_program.main_statements) {
            output.append_printf (
                "%d\t%d\t%s\t%s\n",
                statement.line,
                statement.column,
                ast_statement_kind_name (statement.kind),
                escape_debug_field (statement.text)
            );
        }
        return output.str;
    }

    private static string ast_statement_kind_name (PhpAstStatementKind kind) {
        switch (kind) {
        case PhpAstStatementKind.COMMENT:
            return "COMMENT";
        case PhpAstStatementKind.EXPRESSION:
            return "EXPRESSION";
        case PhpAstStatementKind.CONTROL:
            return "CONTROL";
        case PhpAstStatementKind.DECLARATION:
            return "DECLARATION";
        default:
            return "UNKNOWN";
        }
    }

    private static string escape_debug_field (string value) {
        return value
            .replace ("\\", "\\\\")
            .replace ("\t", "\\t")
            .replace ("\r", "\\r")
            .replace ("\n", "\\n");
    }

    public static PhpAstProgram parse_ast (SourceParts source_parts) throws Error {
        var ast_program = new PhpAstProgram (
            parse_ast_statements (source_parts.main_source),
            source_parts.functions,
            source_parts.classes
        );

        foreach (LocalFunction local_function in source_parts.functions) {
            ast_program.function_statements[local_function.name] = parse_ast_statements (
                local_function.body,
                local_function.line + 1
            );
        }
        foreach (PhpClass php_class in source_parts.classes) {
            foreach (LocalFunction method in php_class.methods) {
                ast_program.method_statements[PhpAstProgram.method_statement_key (php_class.name, method.name)] = parse_ast_statements (
                    method.body,
                    method.line + 1
                );
            }
        }

        return ast_program;
    }

    public static ArrayList<PhpAstStatement> parse_ast_statements (string body, int first_line_number = 1) throws Error {
        return parse_ast_statements_from_tokens (
            PhpToValaTokenizer.tokenize_php_source ("php2vala-inline", body, true, first_line_number)
        );
    }

    public static ArrayList<PhpAstStatement> parse_ast_statements_from_tokens (ArrayList<PhpSourceToken> tokens) {
        var ast_statements = new ArrayList<PhpAstStatement> ();
        var statements = collect_token_statements (tokens);

        for (int i = 0; i < statements.texts.size; i++) {
            string text = statements.texts[i];
            ast_statements.add (new PhpAstStatement (
                text,
                statements.lines[i],
                statements.columns[i],
                classify_statement_kind (text)
            ));
        }

        return ast_statements;
    }

    public static Statements statements_from_ast (ArrayList<PhpAstStatement> ast_statements) {
        var statements = new Statements ();

        foreach (PhpAstStatement statement in ast_statements) {
            statements.texts.add (statement.text);
            statements.lines.add (statement.line);
            statements.columns.add (statement.column);
        }

        return statements;
    }

    private static PhpAstStatementKind classify_statement_kind (string statement_text) {
        string statement = statement_text.strip ();
        if (statement == "") {
            return PhpAstStatementKind.UNKNOWN;
        }
        if (statement.has_prefix ("//") || statement.has_prefix ("#") || statement.has_prefix ("/*")) {
            return PhpAstStatementKind.COMMENT;
        }

        string lowered = statement.down ();
        string first_word = lowered;
        int space_index = first_word.index_of_char (' ');
        int paren_index = first_word.index_of_char ('(');
        int brace_index = first_word.index_of_char ('{');
        int cut_index = first_positive_index (space_index, paren_index, brace_index);
        if (cut_index >= 0) {
            first_word = first_word.substring (0, cut_index);
        }

        switch (first_word) {
        case "class":
        case "const":
        case "enum":
        case "function":
        case "interface":
        case "namespace":
        case "trait":
        case "use":
            return PhpAstStatementKind.DECLARATION;
        case "break":
        case "case":
        case "catch":
        case "continue":
        case "default":
        case "do":
        case "else":
        case "elseif":
        case "exit":
        case "finally":
        case "for":
        case "foreach":
        case "if":
        case "return":
        case "switch":
        case "throw":
        case "try":
        case "while":
        case "die":
            return PhpAstStatementKind.CONTROL;
        default:
            return PhpAstStatementKind.EXPRESSION;
        }
    }

    private static int first_positive_index (int a, int b, int c) {
        int result = -1;
        if (a >= 0) {
            result = a;
        }
        if (b >= 0 && (result < 0 || b < result)) {
            result = b;
        }
        if (c >= 0 && (result < 0 || c < result)) {
            result = c;
        }
        return result;
    }

    private static Statements collect_token_statements (ArrayList<PhpSourceToken> tokens) {
        var statements = new Statements ();
        StringBuilder? pending = null;
        int pending_line = 1;
        int pending_column = 1;
        int paren_depth = 0;
        int closure_brace_depth = 0;
        int string_interpolation_brace_depth = 0;

        foreach (PhpSourceToken token in tokens) {
            if (token.is_token ("T_OPEN_TAG") || token.is_token ("T_CLOSE_TAG")) {
                continue;
            }

            if (token.is_token ("T_WHITESPACE")) {
                if (pending != null) {
                    pending.append (token.text);
                }
                continue;
            }

            if (token.is_token ("T_COMMENT") || token.is_token ("T_DOC_COMMENT")) {
                if (pending == null) {
                    add_token_statement (statements, token.text, token.line, token.column);
                }
                continue;
            }

            if (pending == null) {
                pending = new StringBuilder ();
                pending_line = token.line;
                pending_column = token.column;
            }

            pending.append (token.text);

            if (token.is_token ("T_CURLY_OPEN")) {
                string_interpolation_brace_depth++;
                continue;
            }
            if (string_interpolation_brace_depth > 0 && token.is_symbol ("}")) {
                string_interpolation_brace_depth--;
                continue;
            }

            if (token.is_symbol ("(") || token.is_symbol ("[")) {
                paren_depth++;
                continue;
            }
            if (token.is_symbol (")") || token.is_symbol ("]")) {
                if (paren_depth > 0) {
                    paren_depth--;
                }
                continue;
            }

            if (token.is_symbol ("{")) {
                if (token_statement_is_closure_assignment (pending.str)) {
                    closure_brace_depth++;
                    continue;
                }
                if (paren_depth == 0) {
                    finish_pending_token_statement (statements, ref pending, pending_line, pending_column);
                }
                continue;
            }

            if (token.is_symbol ("}")) {
                if (closure_brace_depth > 0) {
                    closure_brace_depth--;
                    continue;
                }
                if (paren_depth == 0) {
                    finish_pending_token_statement (statements, ref pending, pending_line, pending_column);
                }
                continue;
            }

            if (token.is_symbol (":") && paren_depth == 0 && closure_brace_depth == 0) {
                string pending_text = ((StringBuilder) pending).str.strip ();
                if (pending_text.has_prefix ("case ") || pending_text == "default:") {
                    finish_pending_token_statement (statements, ref pending, pending_line, pending_column);
                    continue;
                }
            }

            if (token.is_symbol (";") && paren_depth == 0 && closure_brace_depth == 0) {
                finish_pending_token_statement (statements, ref pending, pending_line, pending_column);
            }
        }

        if (pending != null) {
            finish_pending_token_statement (statements, ref pending, pending_line, pending_column);
        }

        merge_do_while_end_statements (statements);
        return statements;
    }

    private static void merge_do_while_end_statements (Statements statements) {
        int i = 0;
        while (i + 1 < statements.texts.size) {
            string current = statements.texts[i].strip ();
            string next = statements.texts[i + 1].strip ();
            if (current == "}" && (next.has_prefix ("while(") || next.has_prefix ("while (")) && next.has_suffix (";")) {
                statements.texts[i] = "} " + next;
                statements.texts.remove_at (i + 1);
                statements.lines.remove_at (i + 1);
                statements.columns.remove_at (i + 1);
                continue;
            }
            i++;
        }
    }

    private static void finish_pending_token_statement (Statements statements, ref StringBuilder? pending, int line, int column) {
        if (pending == null) {
            return;
        }
        add_token_statement (statements, ((StringBuilder) pending).str, line, column);
        pending = null;
    }

    private static void add_token_statement (Statements statements, string text, int line, int column) {
        string clean = text.strip ();
        if (clean == "") {
            return;
        }
        statements.texts.add (clean);
        statements.lines.add (line);
        statements.columns.add (column);
    }

    private static bool token_statement_is_closure_assignment (string statement_text) {
        string statement = statement_text.strip ();
        return statement.has_prefix ("$")
            && statement.index_of ("=") > 0
            && statement.index_of ("function") > statement.index_of ("=");
    }

}
