using Gee;

public class PhpToValaZendOpcodeEntry : Object {
    public string op_array_label { get; private set; }
    public int index { get; private set; }
    public string name { get; private set; }
    public int opcode_id { get; private set; }
    public int lookup_id { get; private set; }
    public int line { get; private set; }

    public PhpToValaZendOpcodeEntry (string op_array_label, int index, string name, int opcode_id, int lookup_id, int line) {
        this.op_array_label = op_array_label;
        this.index = index;
        this.name = name;
        this.opcode_id = opcode_id;
        this.lookup_id = lookup_id;
        this.line = line;
    }
}

public class PhpToValaZendOpArrayFrontend : Object {
    public static string debug_op_array_dump (string source_path) throws Error {
        return PhpEmbed.op_array_dump_file_static (source_path);
    }

    public static string opcode_name_for_id (uint opcode) {
        return PhpEmbed.opcode_name_static (opcode);
    }

    public static int opcode_id_for_name (string opcode_name) {
        return PhpEmbed.opcode_id_static (opcode_name);
    }

    public static string? handler_name_for_index (int handler_index) {
        return PhpToValaZendOpcodeHandlers.handler_name_for_index (handler_index);
    }

    public static ArrayList<PhpToValaZendOpcodeEntry> parse_opcode_dump (string dump) {
        var entries = new ArrayList<PhpToValaZendOpcodeEntry> ();

        string current_op_array_label = "main";
        foreach (string raw_line in dump.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix ("op_array ")) {
                string[] header_parts = line.split (" ");
                if (header_parts.length >= 2 && header_parts[1] != "") {
                    current_op_array_label = header_parts[1];
                }
                continue;
            }
            if (!line.has_prefix ("#")) {
                continue;
            }

            string[] parts = line.split (" ");
            var compact = new ArrayList<string> ();
            foreach (string part in parts) {
                if (part != "") {
                    compact.add (part);
                }
            }
            if (compact.size < 2) {
                continue;
            }

            int index = parse_prefixed_int (compact[0], "#");
            string name = compact[1];
            int opcode_id = parse_keyed_int (compact, "opcode=");
            int lookup_id = parse_keyed_int (compact, "lookup_id=");
            int source_line = parse_keyed_int (compact, "line=");
            entries.add (new PhpToValaZendOpcodeEntry (current_op_array_label, index, name, opcode_id, lookup_id, source_line));
        }

        return entries;
    }

    public static void validate_opcode_lookup_ids (string dump) throws Error {
        foreach (PhpToValaZendOpcodeEntry entry in parse_opcode_dump (dump)) {
            if (entry.lookup_id >= 0 && entry.lookup_id != entry.opcode_id) {
                throw new PhpToValaError.UNSUPPORTED (
                    "Zend opcode lookup mismatch for %s: opcode=%d lookup_id=%d".printf (
                        entry.name,
                        entry.opcode_id,
                        entry.lookup_id
                    )
                );
            }
        }
    }

    public static string debug_match_dump (string dump, PhpAstProgram ast_program, PhpIrProgram ir_program, ArrayList<PhpSourceToken>? tokens = null) {
        var entries = parse_opcode_dump (dump);
        var output = new StringBuilder ();
        output.append ("# php2vala combined frontend match dump\n");
        if (tokens != null) {
            output.append_printf ("tokens\t%d\n", ((ArrayList<PhpSourceToken>) tokens).size);
        }
        output.append_printf ("zend_opcodes\t%d\n", entries.size);
        output.append_printf ("ast_main_statements\t%d\n", ast_program.main_statements.size);
        output.append_printf ("ir_main_statements\t%d\n", ir_program.main_statements.size);
        output.append_printf ("ir_functions\t%d\n", ir_program.functions.size);
        output.append_printf ("ir_classes\t%d\n", ir_program.classes.size);
        output.append ("op_array\tindex\topcode\topcode_id\tlookup_id\tline\ttokens\tast_kind\tast_text\tir_kind\tir_text\n");

        foreach (PhpToValaZendOpcodeEntry entry in entries) {
            string token_text = tokens == null ? "" : tokens_for_line ((ArrayList<PhpSourceToken>) tokens, entry.line);
            string ast_kind = "";
            string ast_text = "";
            string ir_kind = "";
            string ir_text = "";
            PhpAstStatement? ast_statement = ast_statement_for_entry (ast_program, entry);
            if (ast_statement != null) {
                ast_kind = ast_statement_kind_name (((PhpAstStatement) ast_statement).kind);
                ast_text = escape_debug_field (((PhpAstStatement) ast_statement).text);
            }
            PhpIrStatement? ir_statement = ir_statement_for_entry (ir_program, entry);
            if (ir_statement != null) {
                ir_kind = ir_statement_kind_name (((PhpIrStatement) ir_statement).kind);
                ir_text = escape_debug_field (((PhpIrStatement) ir_statement).text);
            }
            output.append_printf (
                "%s\t%d\t%s\t%d\t%d\t%d\t%s\t%s\t%s\t%s\t%s\n",
                escape_debug_field (entry.op_array_label),
                entry.index,
                entry.name,
                entry.opcode_id,
                entry.lookup_id,
                entry.line,
                escape_debug_field (token_text),
                ast_kind,
                ast_text,
                ir_kind,
                ir_text
            );
        }

        return output.str;
    }

    private static PhpAstStatement? ast_statement_for_entry (PhpAstProgram ast_program, PhpToValaZendOpcodeEntry entry) {
        ArrayList<PhpAstStatement> statements = ast_statements_for_op_array (ast_program, entry.op_array_label);
        foreach (PhpAstStatement statement in statements) {
            if (statement.line == entry.line) {
                return statement;
            }
        }
        if (entry.op_array_label != "main") {
            foreach (PhpAstStatement statement in statements) {
                if (statement.line == entry.line + 1) {
                    return statement;
                }
            }
        }
        return null;
    }

    private static PhpIrStatement? ir_statement_for_entry (PhpIrProgram ir_program, PhpToValaZendOpcodeEntry entry) {
        ArrayList<PhpIrStatement> statements = ir_statements_for_op_array (ir_program, entry.op_array_label);
        foreach (PhpIrStatement statement in statements) {
            if (statement.source_line == entry.line) {
                return statement;
            }
        }
        if (entry.op_array_label != "main") {
            foreach (PhpIrStatement statement in statements) {
                if (statement.source_line == entry.line + 1) {
                    return statement;
                }
            }
        }
        return null;
    }

    private static ArrayList<PhpAstStatement> ast_statements_for_op_array (PhpAstProgram ast_program, string op_array_label) {
        if (op_array_label.has_prefix ("function:")) {
            string function_name = op_array_label.substring ("function:".length);
            ArrayList<PhpAstStatement>? statements = ast_program.function_statements[function_name];
            return statements ?? new ArrayList<PhpAstStatement> ();
        }
        if (op_array_label.has_prefix ("class:")) {
            string method_key = op_array_label.substring ("class:".length);
            ArrayList<PhpAstStatement>? statements = ast_program.method_statements[method_key];
            return statements ?? new ArrayList<PhpAstStatement> ();
        }
        return ast_program.main_statements;
    }

    private static ArrayList<PhpIrStatement> ir_statements_for_op_array (PhpIrProgram ir_program, string op_array_label) {
        if (op_array_label.has_prefix ("function:")) {
            string function_name = op_array_label.substring ("function:".length);
            ArrayList<PhpIrStatement>? statements = ir_program.function_statements[function_name];
            return statements ?? new ArrayList<PhpIrStatement> ();
        }
        if (op_array_label.has_prefix ("class:")) {
            string method_key = op_array_label.substring ("class:".length);
            ArrayList<PhpIrStatement>? statements = ir_program.method_statements[method_key];
            return statements ?? new ArrayList<PhpIrStatement> ();
        }
        return ir_program.main_statements;
    }

    private static string tokens_for_line (ArrayList<PhpSourceToken> tokens, int line) {
        var output = new StringBuilder ();
        bool first = true;
        foreach (PhpSourceToken token in tokens) {
            if (token.line != line || token.is_token ("T_WHITESPACE") || token.is_token ("T_OPEN_TAG") || token.is_token ("T_CLOSE_TAG")) {
                continue;
            }
            if (!first) {
                output.append (" ");
            }
            first = false;
            output.append (token.name);
            output.append (":");
            output.append (quote_inline_token (token.text));
        }
        return output.str;
    }

    private static string quote_inline_token (string text) {
        string escaped = text
            .replace ("\\", "\\\\")
            .replace ("\t", "\\t")
            .replace ("\r", "\\r")
            .replace ("\n", "\\n");
        if (escaped.length > 48) {
            escaped = escaped.substring (0, 45) + "...";
        }
        return escaped;
    }

    private static int parse_prefixed_int (string value, string prefix) {
        if (!value.has_prefix (prefix)) {
            return -1;
        }
        return parse_int_or_minus_one (value.substring (prefix.length));
    }

    private static int parse_keyed_int (ArrayList<string> parts, string key) {
        foreach (string part in parts) {
            if (part.has_prefix (key)) {
                return parse_int_or_minus_one (part.substring (key.length));
            }
        }
        return -1;
    }

    private static int parse_int_or_minus_one (string value) {
        int64 parsed = 0;
        if (!int64.try_parse (value, out parsed)) {
            return -1;
        }
        return (int) parsed;
    }

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

    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");
    }

}
