#include "moar.h"

/* This representation's function pointer table. */
static const MVMREPROps MVMCapture_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, &MVMCapture_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(MVMCapture);
    }

    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) {
    MVMCaptureBody *src_body  = (MVMCaptureBody *)src;
    MVMCaptureBody *dest_body = (MVMCaptureBody *)dest;
    dest_body->callsite = src_body->callsite->is_interned
        ? src_body->callsite
        : MVM_callsite_copy(tc, src_body->callsite);
    size_t arg_size = dest_body->callsite->flag_count * sizeof(MVMRegister);
    if (arg_size) {
        dest_body->args = MVM_malloc(arg_size);
        memcpy(dest_body->args, src_body->args, arg_size);
    }
    else {
        dest_body->args = NULL;
    }
}

/* Adds held objects to the GC worklist. */
static void gc_mark(MVMThreadContext *tc, MVMSTable *st, void *data, MVMGCWorklist *worklist) {
    MVMCaptureBody *body = (MVMCaptureBody *)data;
    MVMuint8 *flags = body->callsite->arg_flags;
    MVMuint16 count = body->callsite->flag_count;
    MVMuint16 i;
    for (i = 0; i < count; i++)
        if (flags[i] & MVM_CALLSITE_ARG_STR || flags[i] & MVM_CALLSITE_ARG_OBJ)
            MVM_gc_worklist_add(tc, worklist, &(body->args[i].o));
    if (!body->callsite->is_interned)
        MVM_callsite_mark(tc, body->callsite, worklist, NULL);
}

/* Called by the VM in order to free memory associated with this object. */
static void gc_free(MVMThreadContext *tc, MVMObject *obj) {
    MVMCapture *capture = (MVMCapture *)obj;
    if (capture->body.args)
        MVM_free(capture->body.args);
    if (capture->body.callsite && !capture->body.callsite->is_interned)
        MVM_callsite_destroy(capture->body.callsite);
}

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 * MVMCapture_initialize(MVMThreadContext *tc) {
    return &MVMCapture_this_repr;
}

static const MVMREPROps MVMCapture_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,
    MVM_REPR_DEFAULT_ASS_FUNCS,
    MVM_REPR_DEFAULT_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 */
    "MVMCapture", /* name */
    MVM_REPR_ID_MVMCapture,
    NULL, /* unmanaged_size */
    NULL, /* describe_refs */
};

/* Form a capture object from an argument description. If the callsite is not an
 * interned one, then it will be copied, since an MVMCapture assumes that it
 * owns a non-interned callsite. */
MVMObject * MVM_capture_from_args(MVMThreadContext *tc, MVMArgs arg_info) {
    /* Allocate capture before we begin, otherwise we might end up with outdated
     * pointeres in args. */
    MVMObject *capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);

    /* Put callsite arguments into a flat buffer. */
    MVMCallsite *callsite = arg_info.callsite;
    MVMRegister *args;
    if (callsite->flag_count) {
        args = MVM_malloc(callsite->flag_count * sizeof(MVMRegister));
        MVMuint16 i;
        for (i = 0; i < callsite->flag_count; i++)
            args[i] = arg_info.source[arg_info.map[i]];
    }
    else {
        args = NULL;
    }

    /* Form capture object. */
    ((MVMCapture *)capture)->body.callsite = callsite->is_interned
        ? callsite
        : MVM_callsite_copy(tc, callsite);
    ((MVMCapture *)capture)->body.args = args;
    return capture;
}

static MVMCapture * validate_capture(MVMThreadContext *tc, MVMObject *capture) {
    if (REPR(capture)->ID != MVM_REPR_ID_MVMCapture)
        MVM_exception_throw_adhoc(tc, "Capture operation requires an MVMCapture");
    if (!IS_CONCRETE(capture))
        MVM_exception_throw_adhoc(tc, "Capture operation requires concreate capture object");
    return (MVMCapture *)capture;
}

/* Extract an MVMArgs structure from the capture. One must ensure the capture
 * lives for the entire lifetime of the MVMArgs that are populated. */
MVMArgs MVM_capture_to_args(MVMThreadContext *tc, MVMObject *capture_obj) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    MVMArgs args;
    args.callsite = capture->body.callsite;
    args.source = capture->body.args;
    args.map = MVM_args_identity_map(tc, args.callsite);
    return args;
}

/* Get the number of positional arguments that the capture has. */
MVMint64 MVM_capture_num_pos_args(MVMThreadContext *tc, MVMObject *capture_obj) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    return capture->body.callsite->num_pos;
}

/* Get the number of arguments (positional and named) that the capture has. */
MVMint64 MVM_capture_num_args(MVMThreadContext *tc, MVMObject *capture_obj) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    return capture->body.callsite->flag_count;
}

/* Get the primitive value kind for a positional argument. */
static MVMint64 flag_to_spec(MVMint64 flag) {
    switch (flag & MVM_CALLSITE_ARG_TYPE_MASK) {
        case MVM_CALLSITE_ARG_INT:
            return MVM_STORAGE_SPEC_BP_INT;
        case MVM_CALLSITE_ARG_UINT:
            return MVM_STORAGE_SPEC_BP_UINT64;
        case MVM_CALLSITE_ARG_NUM:
            return MVM_STORAGE_SPEC_BP_NUM;
        case MVM_CALLSITE_ARG_STR:
            return MVM_STORAGE_SPEC_BP_STR;
        default:
            return MVM_STORAGE_SPEC_BP_NONE;
    }
}
MVMint64 MVM_capture_arg_pos_primspec(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposprimspec", idx, capture->body.callsite->num_pos);
    return flag_to_spec(capture->body.callsite->arg_flags[idx]);
}

/* Get the primitive value kind for an argument. */
MVMint64 MVM_capture_arg_primspec(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->flag_count)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u)", idx, capture->body.callsite->flag_count);
    return flag_to_spec(capture->body.callsite->arg_flags[idx]);
}

/* Access a positional object argument of an argument capture object. */
MVMObject * MVM_capture_arg_pos_o(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposarg", idx, capture->body.callsite->num_pos);
    if ((capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK) != MVM_CALLSITE_ARG_OBJ)
        MVM_exception_throw_adhoc(tc, "Capture argument is not an object argument for captureposarg. Got %d instead",
            (capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK));
    return capture->body.args[idx].o;
}

/* Access an object argument of an argument capture object. */
MVMObject * MVM_capture_arg_o(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->flag_count)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u)", idx, capture->body.callsite->flag_count);
    if ((capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK) != MVM_CALLSITE_ARG_OBJ)
        MVM_exception_throw_adhoc(tc, "Capture argument is not an object argument");
    return capture->body.args[idx].o;
}

/* Access a positional string argument of an argument capture object. */
MVMString * MVM_capture_arg_pos_s(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposarg_s", idx, capture->body.callsite->num_pos);
    if ((capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK) != MVM_CALLSITE_ARG_STR)
        MVM_exception_throw_adhoc(tc, "Capture argument is not a string argument for captureposarg_s");
    return capture->body.args[idx].s;
}

/* Access a positional integer argument of an argument capture object. */
MVMint64 MVM_capture_arg_pos_i(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposarg_i", idx, capture->body.callsite->num_pos);
    if (!(capture->body.callsite->arg_flags[idx] & (MVM_CALLSITE_ARG_INT | MVM_CALLSITE_ARG_UINT)))
        MVM_exception_throw_adhoc(tc, "Capture argument is not an integer argument for captureposarg_i");
    return capture->body.args[idx].i64;
}

/* Access a positional unsigned integer argument of an argument capture object. */
MVMuint64 MVM_capture_arg_pos_u(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposarg_u", idx, capture->body.callsite->num_pos);
    if (!(capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_INT))
        MVM_exception_throw_adhoc(tc, "Capture argument is not an integer argument for captureposarg_u");
    return capture->body.args[idx].u64;
}

/* Access a positional number argument of an argument capture object. */
MVMnum64 MVM_capture_arg_pos_n(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u) for captureposarg_n", idx, capture->body.callsite->num_pos);
    if ((capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK) != MVM_CALLSITE_ARG_NUM)
        MVM_exception_throw_adhoc(tc, "Capture argument is not a number argument for captureposarg_n");
    return capture->body.args[idx].n64;
}

/* Obtain a positional argument's value and type together. */
void MVM_capture_arg_pos(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx,
        MVMRegister *arg_out, MVMCallsiteFlags *arg_type_out) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u)", idx, capture->body.callsite->num_pos);
    *arg_out = capture->body.args[idx];
    *arg_type_out = capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK;
}

/* Obtain an argument's value and type together. */
void MVM_capture_arg(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx,
        MVMRegister *arg_out, MVMCallsiteFlags *arg_type_out) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->flag_count)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..^%u)", idx, capture->body.callsite->flag_count);
    *arg_out = capture->body.args[idx];
    *arg_type_out = capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK;
}

/* Checks if the capture has a named arg with the specified name. */
MVMint64 MVM_capture_has_named_arg(MVMThreadContext *tc, MVMObject *capture_obj, MVMString *name) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    MVMCallsite *cs = capture->body.callsite;
    MVMuint32 num_nameds = cs->flag_count - cs->num_pos;
    MVMuint32 i;
    for (i = 0; i < num_nameds; i++)
        if (MVM_string_equal(tc, cs->arg_names[i], name))
            return 1;
    return 0;
}

/* Checks if the capture has nameds at all. */
MVMint64 MVM_capture_has_nameds(MVMThreadContext *tc, MVMObject *capture_obj) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    MVMCallsite *cs = capture->body.callsite;
    return cs->flag_count != cs->num_pos;
}

/* Gets a string array of the nameds that the capture has. Evaluates to a type
 * object if there are no nameds. */
MVMObject * MVM_capture_get_names_list(MVMThreadContext *tc, MVMObject *capture_obj) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    MVMCallsite *cs = capture->body.callsite;
    MVMuint16 num_nameds = cs->flag_count - cs->num_pos;
    if (num_nameds == 0)
        return tc->instance->boot_types.BOOTStrArray;
    MVMObject *result = MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTStrArray);
    for (MVMuint16 i = 0; i < num_nameds; i++)
        MVM_repr_bind_pos_s(tc, result, i, cs->arg_names[i]);
    return result;
}

/* Get a hash of the named arguments. */
MVMObject * MVM_capture_get_nameds(MVMThreadContext *tc, MVMObject *capture) {
    /* Set up an args processing context and use the standard slurpy args
     * handler to extract all nameds */
    MVMObject *result;
    MVMROOT(tc, capture) {
        MVMArgs capture_args = MVM_capture_to_args(tc, capture);
        MVMArgProcContext capture_ctx;
        MVM_args_proc_setup(tc, &capture_ctx, capture_args);
        result = MVM_args_slurpy_named(tc, &capture_ctx);
        MVM_args_proc_cleanup(tc, &capture_ctx);
    }
    return result;
}

/* Obtain an argument by its flag index (that is, positional arguments have
 * their positions, and then names are according the order that the names
 * appear in in the callsite's argument name list). */
void MVM_capture_arg_by_flag_index(MVMThreadContext *tc, MVMObject *capture_obj,
        MVMuint32 idx, MVMRegister *arg_out, MVMCallsiteFlags *arg_type_out) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx >= capture->body.callsite->flag_count)
        MVM_exception_throw_adhoc(tc, "Capture argument flag index (%u) out of range (0..^%u)", idx, capture->body.callsite->flag_count);
    *arg_out = capture->body.args[idx];
    *arg_type_out = capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK;
}

/* Check if the argument at the given position is marked as literal. */
MVMint64 MVM_capture_is_literal_arg(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    return (capture->body.callsite->arg_flags[idx] & MVM_CALLSITE_ARG_LITERAL) ? 1 : 0;
}

/* Produce a new capture by taking the current one and dropping the specified
 * positional argument from it. */
MVMObject * MVM_capture_drop_args(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx, MVMuint32 count) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx + count > capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index (%u) out of range (0..%u)", idx + count, capture->body.callsite->num_pos);

    /* Allocate a new capture before we begin; this is the only GC allocation
     * we do. */
    MVMObject *new_capture;
    MVMROOT(tc, capture) {
        new_capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);
    }

    /* We need a callsite without the arguments that are being dropped. */
    MVMCallsite *new_callsite = MVM_callsite_drop_positionals(tc, capture->body.callsite, idx, count);

    /* Form a new arguments buffer, dropping the specified argument. */
    MVMRegister *new_args;
    if (new_callsite->flag_count) {
        new_args = MVM_malloc(new_callsite->flag_count * sizeof(MVMRegister));
        MVMuint32 from, to = 0;
        for (from = 0; from < capture->body.callsite->flag_count; from++) {
            if (from < idx || from >= idx + count) {
                new_args[to] = capture->body.args[from];
                to++;
            }
        }
    }
    else {
        new_args = NULL;
    }

    /* Form new capture object. */
    ((MVMCapture *)new_capture)->body.callsite = new_callsite;
    ((MVMCapture *)new_capture)->body.args = new_args;
    return new_capture;
}

/* Produce a new capture by taking the current one and inserting the specified
 * arg into it. */
MVMObject * MVM_capture_insert_arg(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx,
        MVMCallsiteFlags kind, MVMRegister value) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx > capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index out of range");

    /* Allocate a new capture before we begin; this is the only GC allocation
     * we do. */
    MVMObject *new_capture;
    MVMROOT(tc, capture) {
        if (kind & (MVM_CALLSITE_ARG_OBJ | MVM_CALLSITE_ARG_STR)) {
            MVMROOT(tc, value.o) {
                new_capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);
            }
        }
        else {
            new_capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);
        }
    }

    /* We need a callsite with the argument that is being inserted. */
    MVMCallsite *new_callsite = MVM_callsite_insert_positional(tc, capture->body.callsite,
            idx, kind);

    /* Form a new arguments buffer, dropping the specified argument. */
    MVMRegister *new_args = MVM_malloc(new_callsite->flag_count * sizeof(MVMRegister));
    MVMuint32 from, to = 0;
    for (from = 0; from < capture->body.callsite->flag_count; from++) {
        if (from == idx) {
            new_args[to] = value;
            to++;
        }
        new_args[to] = capture->body.args[from];
        to++;
    }
    if (from == idx)
        new_args[to] = value;

    /* Form new capture object. */
    ((MVMCapture *)new_capture)->body.callsite = new_callsite;
    ((MVMCapture *)new_capture)->body.args = new_args;
    return new_capture;
}

/* Produce a new capture by taking the current one and replacing a designated
 * argument with a new value.
 *
 * The callsite argument type is expected to be the same. */
MVMObject * MVM_capture_replace_arg(MVMThreadContext *tc, MVMObject *capture_obj, MVMuint32 idx,
        MVMCallsiteEntry kind, MVMRegister value) {
    MVMCapture *capture = validate_capture(tc, capture_obj);
    if (idx > capture->body.callsite->num_pos)
        MVM_exception_throw_adhoc(tc, "Capture argument index out of range");

    /* Allocate a new capture before we begin; this is the only GC allocation
     * we do. */
    MVMObject *new_capture;
    MVMROOT(tc, capture) {
        if (kind & (MVM_CALLSITE_ARG_OBJ | MVM_CALLSITE_ARG_STR)) {
            MVMROOT(tc, value.o) {
                new_capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);
            }
        }
        else {
            new_capture = MVM_repr_alloc(tc, tc->instance->boot_types.BOOTCapture);
        }
    }

    /* We need a new callsite with the argument flag replaced.
     * The callsite MUST be created after we allocated as it may contain named
     * arguments, i.e. contain pointers to strings which wouldn't get marked. */
    MVMCallsite *callsite = capture->body.callsite;
    if ((callsite->arg_flags[idx] & MVM_CALLSITE_ARG_TYPE_MASK) != kind)
        MVM_exception_throw_adhoc(tc, "Cannot replace capture argument with different kind %d -> %d", callsite->arg_flags[idx], kind);

    MVMCallsite *new_callsite = MVM_callsite_replace_positional(tc, callsite, idx, kind);
    new_callsite->arg_flags[idx] = kind;

    /* Form a new arguments buffer, replacing the specified argument. */
    MVMRegister *new_args = MVM_malloc(callsite->flag_count * sizeof(MVMRegister));
    MVMuint32 from = 0;
    for (from = 0; from < capture->body.callsite->flag_count; from++) {
        new_args[from] = capture->body.args[from];
    }
    new_args[idx] = value;

    /* Form new capture object. */
    ((MVMCapture *)new_capture)->body.callsite = new_callsite;
    ((MVMCapture *)new_capture)->body.args = new_args;
    return new_capture;
}
