#include "moar.h"

/* This representation's function pointer table. */
static const MVMREPROps MVMContext_this_repr;

/* Creates a new type object of this representation, and associates it with
 * the given HOW. Also sets the invocation protocol handler in the STable. */
static MVMObject * type_object_for(MVMThreadContext *tc, MVMObject *HOW) {
    MVMSTable *st = MVM_gc_allocate_stable(tc, &MVMContext_this_repr, HOW);

    MVMROOT(tc, st, {
        MVMObject *obj = MVM_gc_allocate_type_object(tc, st);
        MVM_ASSIGN_REF(tc, &(st->header), st->WHAT, obj);
        st->size = sizeof(MVMContext);
    });

    return st->WHAT;
}

/* Copies the body of one object to another. */
static void copy_to(MVMThreadContext *tc, MVMSTable *st, void *src, MVMObject *dest_root, void *dest) {
    MVM_exception_throw_adhoc(tc, "Cannot clone an MVMContext");
}

/* Adds held objects to the GC worklist. */
static void gc_mark(MVMThreadContext *tc, MVMSTable *st, void *data, MVMGCWorklist *worklist) {
    MVMContextBody *body = (MVMContextBody *)data;
    MVM_gc_worklist_add(tc, worklist, &body->context);
}

/* Called by the VM in order to free memory associated with this object. */
static void gc_free(MVMThreadContext *tc, MVMObject *obj) {
    MVM_free(((MVMContext *)obj)->body.traversals);
}

static MVMint32 apply_traversals(MVMThreadContext *tc, MVMSpeshFrameWalker *fw, MVMuint8 *traversals,
                                 MVMuint32 num_traversals) {
    MVMuint32 i;
    MVMuint32 could_move = 1;
    for (i = 0; i < num_traversals; i++) {
        switch (traversals[i]) {
            case MVM_CTX_TRAV_OUTER:
                could_move = MVM_spesh_frame_walker_move_outer(tc, fw);
                break;
            case MVM_CTX_TRAV_CALLER:
                could_move = MVM_spesh_frame_walker_move_caller(tc, fw);
                break;
            case MVM_CTX_TRAV_OUTER_SKIP_THUNKS:
                could_move = MVM_spesh_frame_walker_move_outer_skip_thunks(tc, fw);
                break;
            case MVM_CTX_TRAV_CALLER_SKIP_THUNKS:
                could_move = MVM_spesh_frame_walker_move_caller_skip_thunks(tc, fw);
                break;
            default:
                MVM_exception_throw_adhoc(tc, "Unrecognized context traversal operation");
        }
        if (!could_move)
            break;
    }
    return could_move;
}

static MVMuint32 setup_frame_walker(MVMThreadContext *tc, MVMSpeshFrameWalker *fw, MVMContextBody *data) {
    MVM_spesh_frame_walker_init(tc, fw, data->context, 0);
    return apply_traversals(tc, fw, data->traversals, data->num_traversals);
}

static void at_key(MVMThreadContext *tc, MVMSTable *st, MVMObject *root, void *data, MVMObject *key, MVMRegister *result, MVMuint16 kind) {
    MVMString      *name  = (MVMString *)key;
    MVMContextBody *body  = (MVMContextBody *)data;

    MVMSpeshFrameWalker fw;
    MVMRegister *found;
    MVMuint16 found_kind;
    MVM_gc_root_temp_push(tc, (MVMCollectable**)&name);
    if (!setup_frame_walker(tc, &fw, body) || !MVM_spesh_frame_walker_get_lex(tc, &fw,
            name, &found, &found_kind, 1, NULL)) {
        MVM_gc_root_temp_pop(tc);
        char *c_name = MVM_string_utf8_encode_C_string(tc, name);
        char *waste[] = { c_name, NULL };
        MVM_exception_throw_adhoc_free(tc, waste,
            "Lexical with name '%s' does not exist in this frame",
            c_name);
    }
    MVM_gc_root_temp_pop(tc);
    MVM_spesh_frame_walker_cleanup(tc, &fw);

    if (found_kind != kind) {
        if (kind == MVM_reg_int64) {
            switch (found_kind) {
                case MVM_reg_int8:
                    result->i64 = found->i8;
                    return;
                case MVM_reg_int16:
                    result->i64 = found->i16;
                    return;
                case MVM_reg_int32:
                    result->i64 = found->i32;
                    return;
            }
        }
        else if (kind == MVM_reg_uint64) {
            switch (found_kind) {
                case MVM_reg_uint8:
                    result->u64 = found->u8;
                    return;
                case MVM_reg_uint16:
                    result->u64 = found->u16;
                    return;
                case MVM_reg_uint32:
                    result->u64 = found->u32;
                    return;
            }
        }
        {
            char *c_name = MVM_string_utf8_encode_C_string(tc, name);
            char *waste[] = { c_name, NULL };
            MVM_exception_throw_adhoc_free(tc, waste,
                "Lexical with name '%s' has a different type in this frame",
                    c_name);
        }
    }
    *result = *found;
}

static void bind_key(MVMThreadContext *tc, MVMSTable *st, MVMObject *root, void *data, MVMObject *key, MVMRegister value, MVMuint16 kind) {
    MVMString      *name  = (MVMString *)key;
    MVMContextBody *body  = (MVMContextBody *)data;

    MVMSpeshFrameWalker fw;
    MVMRegister *found;
    MVMuint16 got_kind;
    MVMFrame *found_frame;
    MVMuint32 needs_root = kind == MVM_reg_obj || kind == MVM_reg_str;
    MVMuint32 missing;
    if (needs_root)
        MVM_gc_root_temp_push(tc, (MVMCollectable **)&(value.o));
    missing = !setup_frame_walker(tc, &fw, body) || !MVM_spesh_frame_walker_get_lex(tc, &fw,
            name, &found, &got_kind, 1, &found_frame);
    if (needs_root)
        MVM_gc_root_temp_pop(tc);
    if (missing) {
        char *c_name = MVM_string_utf8_encode_C_string(tc, name);
        char *waste[] = { c_name, NULL };
        MVM_exception_throw_adhoc_free(tc, waste,
            "Lexical with name '%s' does not exist in this frame",
            c_name);
    }
    MVM_spesh_frame_walker_cleanup(tc, &fw);

    if (got_kind != kind) {
        char *c_name = MVM_string_utf8_encode_C_string(tc, name);
        char *waste[] = { c_name, NULL };
        MVM_exception_throw_adhoc_free(tc, waste,
            "Lexical with name '%s' has a different type in this frame",
                c_name);
    }

    if (got_kind == MVM_reg_obj || got_kind == MVM_reg_str) {
        MVM_ASSIGN_REF(tc, &(found_frame->header), found->o, value.o);
    }
    else {
        *found = value;
    }
}

static MVMuint64 elems(MVMThreadContext *tc, MVMSTable *st, MVMObject *root, void *data) {
    MVMContextBody *body  = (MVMContextBody *)data;

    MVMSpeshFrameWalker fw;
    MVMuint64 result = setup_frame_walker(tc, &fw, body)
        ? MVM_spesh_frame_walker_get_lexical_count(tc, &fw)
        : 0;
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return result;
}

static MVMint64 exists_key(MVMThreadContext *tc, MVMSTable *st, MVMObject *root, void *data, MVMObject *key) {
    MVMContextBody *body = (MVMContextBody *)data;

    MVMSpeshFrameWalker fw;
    MVMRegister *found;
    MVMuint16 found_kind;
    MVMuint64 result = setup_frame_walker(tc, &fw, body) && MVM_spesh_frame_walker_get_lex(tc, &fw,
            (MVMString *)key, &found, &found_kind, 0, NULL);
    MVM_spesh_frame_walker_cleanup(tc, &fw);

    return result;
}

static void delete_key(MVMThreadContext *tc, MVMSTable *st, MVMObject *root, void *data, MVMObject *key) {
    MVM_exception_throw_adhoc(tc,
        "MVMContext representation does not support delete key");
}

static MVMStorageSpec get_value_storage_spec(MVMThreadContext *tc, MVMSTable *st) {
    MVMStorageSpec spec;
    spec.inlineable      = MVM_STORAGE_SPEC_REFERENCE;
    spec.boxed_primitive = MVM_STORAGE_SPEC_BP_NONE;
    spec.can_box         = 0;
    spec.bits            = 0;
    spec.align           = 0;
    spec.is_unsigned     = 0;

    return spec;
}

static const MVMStorageSpec storage_spec = {
    MVM_STORAGE_SPEC_REFERENCE, /* inlineable */
    0,                          /* bits */
    0,                          /* align */
    MVM_STORAGE_SPEC_BP_NONE,   /* boxed_primitive */
    0,                          /* can_box */
    0,                          /* is_unsigned */
};


/* Gets the storage specification for this representation. */
static const MVMStorageSpec * get_storage_spec(MVMThreadContext *tc, MVMSTable *st) {
    return &storage_spec;
}

/* Compose the representation. */
static void compose(MVMThreadContext *tc, MVMSTable *st, MVMObject *info) {
    /* Nothing to do for this REPR. */
}

/* Initializes the representation. */
const MVMREPROps * MVMContext_initialize(MVMThreadContext *tc) {
    return &MVMContext_this_repr;
}

static const MVMREPROps MVMContext_this_repr = {
    type_object_for,
    MVM_gc_allocate_object,
    NULL, /* initialize */
    copy_to,
    MVM_REPR_DEFAULT_ATTR_FUNCS,
    MVM_REPR_DEFAULT_BOX_FUNCS,
    MVM_REPR_DEFAULT_POS_FUNCS,
    {
        at_key,
        bind_key,
        exists_key,
        delete_key,
        get_value_storage_spec
    },   /* ass_funcs */
    elems,
    get_storage_spec,
    NULL, /* change_type */
    NULL, /* serialize */
    NULL, /* deserialize */
    NULL, /* serialize_repr_data */
    NULL, /* deserialize_repr_data */
    NULL, /* deserialize_stable_size */
    gc_mark,
    gc_free,
    NULL, /* gc_cleanup */
    NULL, /* gc_mark_repr_data */
    NULL, /* gc_free_repr_data */
    compose,
    NULL, /* spesh */
    "MVMContext", /* name */
    MVM_REPR_ID_MVMContext,
    NULL, /* unmanaged_size */
    NULL, /* describe_refs */
};

/* Walks through the frames and, upon encountering one whose caller has
 * inlines, records the deopt index or JIT location, so we will be able
 * to properly reconstruct the call tree. Stops once it finds a place we
 * already calculated this information. */
static void snapshot_frame_callees(MVMThreadContext *tc, MVMFrame *f) {
    while (f && f->caller) {
        MVMSpeshCandidate *cand = f->caller->spesh_cand;
        MVMFrameExtra *extra = MVM_frame_extra(tc, f);
        extra->caller_info_needed = 1;
        extra->caller_pos_needed = 1;
        if (cand && cand->body.num_inlines) {
            if (cand->body.jitcode) {
                if (extra->caller_jit_position)
                    return;
                extra->caller_jit_position = MVM_jit_code_get_current_position(tc, cand->body.jitcode, f->caller);
            }
            else {
                if (extra->caller_deopt_idx)
                    return;
                /* Store with + 1 to avoid semi-predicate problem. */
                extra->caller_deopt_idx = 1 + MVM_spesh_deopt_find_inactive_frame_deopt_idx(tc, f->caller, cand);
            }
        }
        f = f->caller;
    }
}

/* Creates a MVMContent wrapper object around an MVMFrame. */
MVMObject * MVM_context_from_frame(MVMThreadContext *tc, MVMFrame *f) {
    MVMObject *ctx;
    f = MVM_frame_force_to_heap(tc, f);
    snapshot_frame_callees(tc, f);
    MVMROOT(tc, f, {
        ctx = MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTContext);
        MVM_ASSIGN_REF(tc, &(ctx->header), ((MVMContext *)ctx)->body.context, f);
        ((MVMContext *)ctx)->body.traversable = 1;
    });
    return ctx;
}

/* Creates a MVMContent wrapper object around an MVMFrame that only allows
 * access to the current frame and does not support traversal. */
MVMObject * MVM_context_from_frame_non_traversable(MVMThreadContext *tc, MVMFrame *f) {
    MVMObject *ctx;
    f = MVM_frame_force_to_heap(tc, f);
    MVMROOT(tc, f, {
        ctx = MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTContext);
        MVM_ASSIGN_REF(tc, &(ctx->header), ((MVMContext *)ctx)->body.context, f);
    });
    return ctx;
}

/* Checks if we can perform a traversal and reach an existing frame. */
static MVMint32 traversal_exists(MVMThreadContext *tc, MVMFrame *base, MVMuint8 *traversals,
                                 MVMuint32 num_traversals) {
    MVMSpeshFrameWalker fw;
    MVMuint32 could_move;
    MVM_spesh_frame_walker_init(tc, &fw, base, 0);
    could_move = apply_traversals(tc, &fw, traversals, num_traversals);
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return could_move;
}

/* Try to get a context with the specified traversal applied. Ensures that the
 * traversal would lead to a result, and returns a NULL context if not. */
MVMObject * MVM_context_apply_traversal(MVMThreadContext *tc, MVMContext *ctx, MVMuint8 traversal) {
    /* Ensure we've got a traversable context. */
    if (!ctx->body.traversable)
        MVM_exception_throw_adhoc(tc,
                "Cannot move to outers or callers with non-traversable context");

    /* Create new traversals array with the latest one at the end. */
    MVMuint32 new_num_traversals = ctx->body.num_traversals + 1;
    MVMuint8 *new_traversals = MVM_malloc(new_num_traversals);
    if (ctx->body.num_traversals)
        memcpy(new_traversals, ctx->body.traversals, ctx->body.num_traversals);
    new_traversals[new_num_traversals - 1] = traversal;

    /* Verify that we can do this traversal. */
    if (traversal_exists(tc, ctx->body.context, new_traversals, new_num_traversals)) {
        /* Yes, make a new context object and return it. */
        MVMContext *result;
        MVMROOT(tc, ctx, {
            result = (MVMContext *)MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTContext);
        });
        MVM_ASSIGN_REF(tc, &(result->common.header), result->body.context, ctx->body.context);
        result->body.traversals = new_traversals;
        result->body.num_traversals = new_num_traversals;
        result->body.traversable = 1;
        return (MVMObject *)result;
    }
    else {
        MVM_free(new_traversals);
        return tc->instance->VMNull;
    }
}

/* Resolves the context to an exact frame. Returns NULL if it resolves to an
 * inline or doesn't resolve. */
MVMFrame * MVM_context_get_frame(MVMThreadContext *tc, MVMContext *ctx) {
    MVMSpeshFrameWalker fw;
    MVMFrame *result = NULL;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals))
        result = MVM_spesh_frame_walker_get_frame(tc, &fw);
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return result;
}

/* Resolves the context to an exact frame; if the frame in question is an
 * inline, takes the inline's outer. Returns NULL if neither resolves. */
MVMFrame * MVM_context_get_frame_or_outer(MVMThreadContext *tc, MVMContext *ctx) {
    MVMSpeshFrameWalker fw;
    MVMFrame *result = NULL;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals)) {
        result = MVM_spesh_frame_walker_get_frame(tc, &fw);
        if (!result) {
            MVM_spesh_frame_walker_move_outer(tc, &fw);
            result = MVM_spesh_frame_walker_get_frame(tc, &fw);
        }
    }
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return result;
}

/* Resolves the context and gets a hash of its lexicals. */
MVMObject * MVM_context_lexicals_as_hash(MVMThreadContext *tc, MVMContext *ctx) {
    MVMSpeshFrameWalker fw;
    MVMObject *result;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals))
        result = MVM_spesh_frame_walker_get_lexicals_hash(tc, &fw);
    else
        result = MVM_repr_alloc_init(tc, MVM_hll_current(tc)->slurpy_hash_type);
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return result;
}

/* Find the primitive lexical type of a lexical in the context. */
MVMint64 MVM_context_lexical_primspec(MVMThreadContext *tc, MVMContext *ctx, MVMString *name) {
    MVMSpeshFrameWalker fw;
    MVMint64 primspec = -1;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals))
        primspec = MVM_spesh_frame_walker_get_lexical_primspec(tc, &fw, name);
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    if (primspec < 0) {
        char *c_name = MVM_string_utf8_encode_C_string(tc, name);
        char *waste[] = { c_name, NULL };
        MVM_exception_throw_adhoc_free(tc, waste, "Frame has no lexical with name '%s'",
            c_name);
    }
    return primspec;
}

/* Finds the code object associated with the frame represented by this context. */
MVMObject * MVM_context_get_code(MVMThreadContext *tc, MVMContext *ctx) {
    MVMSpeshFrameWalker fw;
    MVMObject *result = NULL;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals))
        result = MVM_spesh_frame_walker_get_code(tc, &fw);
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return result ? result : tc->instance->VMNull;
}

/* Does a lexical lookup relative to the context's current location.
 * Evaluates to a VMNull if it's not found. */
MVMObject * MVM_context_lexical_lookup(MVMThreadContext *tc, MVMContext *ctx, MVMString *name) {
    MVMSpeshFrameWalker fw;
    MVM_spesh_frame_walker_init_for_outers(tc, &fw, ctx->body.context);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals)) {
        MVMRegister *result = MVM_frame_lexical_lookup_using_frame_walker(tc, &fw, name, MVM_reg_obj);
        return result ? result->o : tc->instance->VMNull;
    }
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return tc->instance->VMNull;
}

/* Does a dynamic lexical lookup relative to the context's current location.
 * Evaluates to a VMNull if it's not found. */
void MVM_context_dynamic_lookup(MVMThreadContext *tc, MVMContext *ctx, MVMString *name, MVMRegister *r) {
    MVMSpeshFrameWalker fw;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 0);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals)) {
        MVM_frame_getdynlex_with_frame_walker(tc, &fw, name, r);
        return;
    }
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    r->o = tc->instance->VMNull;
}

/* Does a caller lexical lookup relative to the context's current location.
 * Evaluates to a VMNull if it's not found. */
MVMObject * MVM_context_caller_lookup(MVMThreadContext *tc, MVMContext *ctx, MVMString *name) {
    MVMSpeshFrameWalker fw;
    MVM_spesh_frame_walker_init(tc, &fw, ctx->body.context, 1);
    if (apply_traversals(tc, &fw, ctx->body.traversals, ctx->body.num_traversals)) {
        MVMRegister *result = MVM_frame_lexical_lookup_using_frame_walker(tc, &fw, name, MVM_reg_obj);
        return result ? result->o : tc->instance->VMNull;
    }
    MVM_spesh_frame_walker_cleanup(tc, &fw);
    return tc->instance->VMNull;
}
