#line <%= __LINE__ + 1 %> "<%= 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(pm_parser_t *parser, const uint8_t *start, const uint8_t *end, VALUE source) {
    VALUE argv[] = { source, LONG2FIX(start - parser->start), LONG2FIX(end - start) };
    return rb_class_new_instance(3, argv, rb_cPrismLocation);
}

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

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

    return rb_class_new_instance(3, argv, rb_cPrismToken);
}

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

// Create a Prism::Source object from the given parser.
VALUE
pm_source_new(pm_parser_t *parser, rb_encoding *encoding) {
    VALUE source = 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, INT2FIX(parser->newline_list.offsets[index]));
    }

    VALUE source_argv[] = { source, LONG2NUM(parser->start_line), offsets };
    return rb_class_new_instance(3, source_argv, rb_cPrismSource);
}

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

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

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

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

    return visit;
}

VALUE
pm_ast_new(pm_parser_t *parser, pm_node_t *node, rb_encoding *encoding) {
    VALUE source = pm_source_new(parser, encoding);
    ID *constants = calloc(parser->constant_pool.size, sizeof(ID));

    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);
        ID value = rb_protect(rb_intern_str, string, &state);

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

        constants[index] = 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;
            }

            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::NodeField, Prism::OptionalNodeField, Prism::NodeListField].include?(field.class) } -%>
#line <%= __LINE__ + 1 %> "<%= 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::NodeField, Prism::OptionalNodeField -%>
                    pm_node_stack_push(&node_stack, (pm_node_t *) cast-><%= field.name %>);
                    <%- when Prism::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 %> "<%= File.basename(__FILE__) %>"
        } else {
            pm_node_t *node = pm_node_stack_pop(&node_stack);

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

                    // <%= field.name %>
                    <%- case field -%>
                    <%- when Prism::NodeField, Prism::OptionalNodeField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = rb_ary_pop(value_stack);
                    <%- when Prism::NodeListField -%>
#line <%= __LINE__ + 1 %> "<%= 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::StringField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = pm_string_new(&cast-><%= field.name %>, encoding);
                    <%- when Prism::ConstantField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    assert(cast-><%= field.name %> != 0);
                    argv[<%= index %>] = rb_id2sym(constants[cast-><%= field.name %> - 1]);
                    <%- when Prism::OptionalConstantField -%>
                    argv[<%= index %>] = cast-><%= field.name %> == 0 ? Qnil : rb_id2sym(constants[cast-><%= field.name %> - 1]);
                    <%- when Prism::ConstantListField -%>
#line <%= __LINE__ + 1 %> "<%= 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 %>], rb_id2sym(constants[cast-><%= field.name %>.ids[index] - 1]));
                    }
                    <%- when Prism::LocationField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = pm_location_new(parser, cast-><%= field.name %>.start, cast-><%= field.name %>.end, source);
                    <%- when Prism::OptionalLocationField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = cast-><%= field.name %>.start == NULL ? Qnil : pm_location_new(parser, cast-><%= field.name %>.start, cast-><%= field.name %>.end, source);
                    <%- when Prism::UInt8Field -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = UINT2NUM(cast-><%= field.name %>);
                    <%- when Prism::UInt32Field -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = ULONG2NUM(cast-><%= field.name %>);
                    <%- when Prism::FlagsField -%>
#line <%= __LINE__ + 1 %> "<%= File.basename(__FILE__) %>"
                    argv[<%= index %>] = ULONG2NUM(node->flags & ~PM_NODE_FLAG_COMMON_MASK);
                    <%- else -%>
                    <%- raise -%>
                    <%- end -%>
                    <%- end -%>

                    // location
                    argv[<%= node.fields.length %>] = pm_location_new(parser, node->location.start, node->location.end, source);

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

    VALUE result = rb_ary_pop(value_stack);
    free(constants);
    return result;
}

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 -%>
}
