#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
#include "prism/extension.h"

extern VALUE rb_cPrism;
extern VALUE rb_cPrismNode;
extern VALUE rb_cPrismSource;
extern VALUE rb_cPrismToken;
extern VALUE rb_cPrismLocation;

<%- nodes.each do |node| -%>
static VALUE rb_cPrism<%= node.name %>;
<%- end -%>

static VALUE
pm_location_new(const pm_parser_t *parser, const uint8_t *start, const uint8_t *end) {
    uint64_t value = ((((uint64_t) (start - parser->start)) << 32) | ((uint32_t) (end - start)));
    return ULL2NUM(value);
}

VALUE
pm_token_new(const pm_parser_t *parser, const pm_token_t *token, rb_encoding *encoding, VALUE source) {
    ID type = rb_intern(pm_token_type_name(token->type));
    VALUE location = pm_location_new(parser, token->start, token->end);

    VALUE argv[] = {
        source,
        ID2SYM(type),
        rb_enc_str_new((const char *) token->start, token->end - token->start, encoding),
        location
    };

    return rb_class_new_instance(4, argv, rb_cPrismToken);
}

static VALUE
pm_string_new(const pm_string_t *string, rb_encoding *encoding) {
    return rb_enc_str_new((const char *) pm_string_source(string), pm_string_length(string), encoding);
}

VALUE
pm_integer_new(const pm_integer_t *integer) {
    VALUE result;
    if (integer->values == NULL) {
        result = UINT2NUM(integer->value);
    } else {
        VALUE string = rb_str_new(NULL, integer->length * 8);
        unsigned char *bytes = (unsigned char *) RSTRING_PTR(string);

        size_t offset = integer->length * 8;
        for (size_t value_index = 0; value_index < integer->length; value_index++) {
            uint32_t value = integer->values[value_index];

            for (int index = 0; index < 8; index++) {
                int byte = (value >> (4 * index)) & 0xf;
                bytes[--offset] = byte < 10 ? byte + '0' : byte - 10 + 'a';
            }
        }

        result = rb_funcall(string, rb_intern("to_i"), 1, UINT2NUM(16));
    }

    if (integer->negative) {
        result = rb_funcall(result, rb_intern("-@"), 0);
    }

    return result;
}

// Create a Prism::Source object from the given parser, after pm_parse() was called.
VALUE
pm_source_new(const pm_parser_t *parser, rb_encoding *encoding) {
    VALUE source_string = rb_enc_str_new((const char *) parser->start, parser->end - parser->start, encoding);

    VALUE offsets = rb_ary_new_capa(parser->newline_list.size);
    for (size_t index = 0; index < parser->newline_list.size; index++) {
        rb_ary_push(offsets, ULONG2NUM(parser->newline_list.offsets[index]));
    }

    return rb_funcall(rb_cPrismSource, rb_intern("for"), 3, source_string, LONG2NUM(parser->start_line), offsets);
}

typedef struct pm_node_stack_node {
    struct pm_node_stack_node *prev;
    const pm_node_t *visit;
    bool visited;
} pm_node_stack_node_t;

static void
pm_node_stack_push(pm_node_stack_node_t **stack, const pm_node_t *visit) {
    pm_node_stack_node_t *node = xmalloc(sizeof(pm_node_stack_node_t));
    node->prev = *stack;
    node->visit = visit;
    node->visited = false;
    *stack = node;
}

static const pm_node_t *
pm_node_stack_pop(pm_node_stack_node_t **stack) {
    pm_node_stack_node_t *current = *stack;
    const pm_node_t *visit = current->visit;

    *stack = current->prev;
    xfree(current);

    return visit;
}

VALUE
pm_ast_new(const pm_parser_t *parser, const pm_node_t *node, rb_encoding *encoding, VALUE source) {
    VALUE constants = rb_ary_new_capa(parser->constant_pool.size);

    for (uint32_t index = 0; index < parser->constant_pool.size; index++) {
        pm_constant_t *constant = &parser->constant_pool.constants[index];
        int state = 0;

        VALUE string = rb_enc_str_new((const char *) constant->start, constant->length, encoding);
        VALUE value = rb_protect(rb_str_intern, string, &state);

        if (state != 0) {
            value = ID2SYM(rb_intern_const("?"));
            rb_set_errinfo(Qnil);
        }

        rb_ary_push(constants, value);
    }

    pm_node_stack_node_t *node_stack = NULL;
    pm_node_stack_push(&node_stack, node);
    VALUE value_stack = rb_ary_new();

    while (node_stack != NULL) {
        if (!node_stack->visited) {
            if (node_stack->visit == NULL) {
                pm_node_stack_pop(&node_stack);
                rb_ary_push(value_stack, Qnil);
                continue;
            }

            const pm_node_t *node = node_stack->visit;
            node_stack->visited = true;

            switch (PM_NODE_TYPE(node)) {
                <%- nodes.each do |node| -%>
                <%- if node.fields.any? { |field| [Prism::Template::NodeField, Prism::Template::OptionalNodeField, Prism::Template::NodeListField].include?(field.class) } -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                case <%= node.type %>: {
                    pm_<%= node.human %>_t *cast = (pm_<%= node.human %>_t *) node;
                    <%- node.fields.each do |field| -%>
                    <%- case field -%>
                    <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%>
                    pm_node_stack_push(&node_stack, (pm_node_t *) cast-><%= field.name %>);
                    <%- when Prism::Template::NodeListField -%>
                    for (size_t index = 0; index < cast-><%= field.name %>.size; index++) {
                        pm_node_stack_push(&node_stack, (pm_node_t *) cast-><%= field.name %>.nodes[index]);
                    }
                    <%- end -%>
                    <%- end -%>
                    break;
                }
                <%- end -%>
                <%- end -%>
                default:
                    break;
            }
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
        } else {
            const pm_node_t *node = pm_node_stack_pop(&node_stack);

            switch (PM_NODE_TYPE(node)) {
                <%- nodes.each do |node| -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                case <%= node.type %>: {
                    <%- if node.fields.any? { |field| ![Prism::Template::NodeField, Prism::Template::OptionalNodeField].include?(field.class) } -%>
                    pm_<%= node.human %>_t *cast = (pm_<%= node.human %>_t *) node;
                    <%- end -%>
                    VALUE argv[<%= node.fields.length + 4 %>];

                    // source
                    argv[0] = source;

                    // node_id
                    argv[1] = ULONG2NUM(node->node_id);

                    // location
                    argv[2] = pm_location_new(parser, node->location.start, node->location.end);

                    // flags
                    argv[3] = ULONG2NUM(node->flags);
                    <%- node.fields.each.with_index(4) do |field, index| -%>

                    // <%= field.name %>
                    <%- case field -%>
                    <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = rb_ary_pop(value_stack);
                    <%- when Prism::Template::NodeListField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = rb_ary_new_capa(cast-><%= field.name %>.size);
                    for (size_t index = 0; index < cast-><%= field.name %>.size; index++) {
                        rb_ary_push(argv[<%= index %>], rb_ary_pop(value_stack));
                    }
                    <%- when Prism::Template::StringField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = pm_string_new(&cast-><%= field.name %>, encoding);
                    <%- when Prism::Template::ConstantField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    assert(cast-><%= field.name %> != 0);
                    argv[<%= index %>] = RARRAY_AREF(constants, cast-><%= field.name %> - 1);
                    <%- when Prism::Template::OptionalConstantField -%>
                    argv[<%= index %>] = cast-><%= field.name %> == 0 ? Qnil : RARRAY_AREF(constants, cast-><%= field.name %> - 1);
                    <%- when Prism::Template::ConstantListField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = rb_ary_new_capa(cast-><%= field.name %>.size);
                    for (size_t index = 0; index < cast-><%= field.name %>.size; index++) {
                        assert(cast-><%= field.name %>.ids[index] != 0);
                        rb_ary_push(argv[<%= index %>], RARRAY_AREF(constants, cast-><%= field.name %>.ids[index] - 1));
                    }
                    <%- when Prism::Template::LocationField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = pm_location_new(parser, cast-><%= field.name %>.start, cast-><%= field.name %>.end);
                    <%- when Prism::Template::OptionalLocationField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = cast-><%= field.name %>.start == NULL ? Qnil : pm_location_new(parser, cast-><%= field.name %>.start, cast-><%= field.name %>.end);
                    <%- when Prism::Template::UInt8Field -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = UINT2NUM(cast-><%= field.name %>);
                    <%- when Prism::Template::UInt32Field -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = ULONG2NUM(cast-><%= field.name %>);
                    <%- when Prism::Template::IntegerField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = pm_integer_new(&cast-><%= field.name %>);
                    <%- when Prism::Template::DoubleField -%>
#line <%= __LINE__ + 1 %> "prism/templates/ext/prism/<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = DBL2NUM(cast-><%= field.name %>);
                    <%- else -%>
                    <%- raise -%>
                    <%- end -%>
                    <%- end -%>

                    rb_ary_push(value_stack, rb_class_new_instance(<%= node.fields.length + 4 %>, argv, rb_cPrism<%= node.name %>));
                    break;
                }
                <%- end -%>
                default:
                    rb_raise(rb_eRuntimeError, "unknown node type: %d", PM_NODE_TYPE(node));
            }
        }
    }

    return rb_ary_pop(value_stack);
}

void
Init_prism_api_node(void) {
    <%- nodes.each do |node| -%>
    rb_cPrism<%= node.name %> = rb_define_class_under(rb_cPrism, "<%= node.name %>", rb_cPrismNode);
    <%- end -%>
}
