
/*
 * Copyright (C) Alexander Borisov
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


typedef enum {
    NJS_ENCODING_UTF8,
} njs_encoding_t;


typedef struct {
    njs_encoding_t        encoding;
    njs_bool_t            fatal;
    njs_bool_t            ignore_bom;

    njs_unicode_decode_t  ctx;
} njs_encoding_decode_t;


typedef struct {
    njs_str_t             name;
    njs_encoding_t        encoding;
} njs_encoding_label_t;


static njs_encoding_label_t  njs_encoding_labels[] =
{
    { njs_str("utf-8"), NJS_ENCODING_UTF8 },
    { njs_str("utf8") , NJS_ENCODING_UTF8 },
    { njs_null_str, 0 }
};


static njs_int_t njs_text_encoder_encode_utf8(njs_vm_t *vm,
    njs_string_prop_t *prop, njs_value_t *retval);
static njs_int_t njs_text_decoder_arg_encoding(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_encoding_decode_t *data);
static njs_int_t njs_text_decoder_arg_options(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_encoding_decode_t *data);


static njs_int_t
njs_text_encoder_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_object_value_t  *encoder;

    if (!vm->top_frame->ctor) {
        njs_type_error(vm, "Constructor of TextEncoder requires 'new'");
        return NJS_ERROR;
    }

    encoder = njs_object_value_alloc(vm, NJS_OBJ_TYPE_TEXT_ENCODER, 0, NULL);
    if (njs_slow_path(encoder == NULL)) {
        return NJS_ERROR;
    }

    njs_set_data(&encoder->value, NULL, NJS_DATA_TAG_TEXT_ENCODER);
    njs_set_object_value(retval, encoder);

    return NJS_OK;
}


static njs_int_t
njs_text_encoder_encode(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    u_char                *dst;
    size_t                size;
    njs_int_t             ret;
    njs_value_t           *this, *input, value;
    const u_char          *start, *end;
    njs_string_prop_t     prop;
    njs_typed_array_t     *array;
    njs_unicode_decode_t  ctx;

    this = njs_argument(args, 0);

    if (njs_slow_path(!njs_is_object_data(this, NJS_DATA_TAG_TEXT_ENCODER))) {
        njs_type_error(vm, "\"this\" is not a TextEncoder");
        return NJS_ERROR;
    }

    start = NULL;
    end = NULL;

    if (nargs > 1) {
        input = njs_argument(args, 1);

        if (njs_slow_path(!njs_is_string(input))) {
            ret = njs_value_to_string(vm, input, input);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }
        }

        (void) njs_string_prop(vm, &prop, input);

        if (prop.length != 0) {
            return njs_text_encoder_encode_utf8(vm, &prop, retval);
        }

        start = prop.start;
        end = start + prop.size;
    }

    njs_utf8_decode_init(&ctx);

    (void) njs_utf8_stream_length(&ctx, start, end - start, 1, 0, &size);

    njs_set_number(&value, size);

    array = njs_typed_array_alloc(vm, &value, 1, 0, NJS_OBJ_TYPE_UINT8_ARRAY);
    if (njs_slow_path(array == NULL)) {
        return NJS_ERROR;
    }

    dst = njs_typed_array_buffer(array)->u.u8;
    njs_utf8_decode_init(&ctx);

    (void) njs_utf8_stream_encode(&ctx, start, end, dst, 1, 0);

    njs_set_typed_array(retval, array);

    return NJS_OK;
}


static njs_int_t
njs_text_encoder_encode_utf8(njs_vm_t *vm, njs_string_prop_t *prop,
    njs_value_t *retval)
{
    njs_value_t        value;
    njs_typed_array_t  *array;

    njs_set_number(&value, prop->size);

    array = njs_typed_array_alloc(vm, &value, 1, 0, NJS_OBJ_TYPE_UINT8_ARRAY);
    if (njs_slow_path(array == NULL)) {
        return NJS_ERROR;
    }

    memcpy(njs_typed_array_buffer(array)->u.u8, prop->start, prop->size);

    njs_set_typed_array(retval, array);

    return NJS_OK;
}


static njs_int_t
njs_text_encoder_encode_into(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    u_char                *to, *to_end;
    size_t                size;
    uint32_t              cp;
    njs_int_t             ret;
    njs_str_t             str;
    njs_value_t           *this, *input, *dest, value, read, written;
    njs_object_t          *object;
    const u_char          *start, *end;
    njs_typed_array_t     *array;
    njs_unicode_decode_t  ctx;

    this = njs_argument(args, 0);
    input = njs_arg(args, nargs, 1);
    dest = njs_arg(args, nargs, 2);

    if (njs_slow_path(!njs_is_object_data(this, NJS_DATA_TAG_TEXT_ENCODER))) {
        njs_type_error(vm, "\"this\" is not a TextEncoder");
        return NJS_ERROR;
    }

    if (njs_slow_path(!njs_is_string(input))) {
        ret = njs_value_to_string(vm, &value, input);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }

        input = &value;
    }

    if (njs_slow_path(!njs_is_typed_array_uint8(dest))) {
        njs_type_error(vm, "The \"destination\" argument must be an instance "
                       "of Uint8Array");
        return NJS_ERROR;
    }

    njs_string_get(vm, input, &str);

    start = str.start;
    end = start + str.length;

    array = njs_typed_array(dest);
    to = njs_typed_array_start(array);
    to_end = to + array->byte_length;

    njs_set_number(&read, 0);
    njs_set_number(&written, 0);

    njs_utf8_decode_init(&ctx);

    while (start < end) {
        cp = njs_utf8_decode(&ctx, &start, end);

        if (cp > NJS_UNICODE_MAX_CODEPOINT) {
            cp = NJS_UNICODE_REPLACEMENT;
        }

        size = njs_utf8_size(cp);

        if (to + size > to_end) {
            break;
        }

        njs_number(&read) += (cp > 0xFFFF) ? 2 : 1;
        njs_number(&written) += size;

        to = njs_utf8_encode(to, cp);
    }

    object = njs_object_alloc(vm);
    if (njs_slow_path(object == NULL)) {
        return NJS_ERROR;
    }

    njs_set_object(retval, object);

    ret = njs_object_prop_define(vm, retval, NJS_ATOM_STRING_read, &read,
                                 NJS_OBJECT_PROP_VALUE_CW);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_object_prop_define(vm, retval, NJS_ATOM_STRING_written, &written,
                                 NJS_OBJECT_PROP_VALUE_CW);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    return NJS_OK;
}


static const njs_object_prop_init_t  njs_text_encoder_properties[] =
{
    NJS_DECLARE_PROP_HANDLER(STRING_constructor,
                             njs_object_prototype_create_constructor, 0,
                             NJS_OBJECT_PROP_VALUE_CW),

    NJS_DECLARE_PROP_VALUE(STRING_encoding, njs_ascii_strval("utf-8"), 0),

    NJS_DECLARE_PROP_NATIVE(STRING_encode, njs_text_encoder_encode, 0, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_encodeInto, njs_text_encoder_encode_into,
                            2, 0),
};


const njs_object_init_t  njs_text_encoder_init = {
    njs_text_encoder_properties,
    njs_nitems(njs_text_encoder_properties),
};


static const njs_object_prop_init_t  njs_text_encoder_constructor_properties[] =
{
    NJS_DECLARE_PROP_LENGTH(0),

    NJS_DECLARE_PROP_NAME("TextEncoder"),

    NJS_DECLARE_PROP_HANDLER(STRING_prototype, njs_object_prototype_create,
                             0, 0),
};


const njs_object_init_t  njs_text_encoder_constructor_init = {
    njs_text_encoder_constructor_properties,
    njs_nitems(njs_text_encoder_constructor_properties),
};


const njs_object_type_init_t  njs_text_encoder_type_init = {
   .constructor = njs_native_ctor(njs_text_encoder_constructor, 0, 0),
   .prototype_props = &njs_text_encoder_init,
   .constructor_props = &njs_text_encoder_constructor_init,
   .prototype_value = { .object = { .type = NJS_OBJECT } },
};


static njs_int_t
njs_text_decoder_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t              ret;
    njs_object_value_t     *decoder;
    njs_encoding_decode_t  *data;

    if (!vm->top_frame->ctor) {
        njs_type_error(vm, "Constructor of TextDecoder requires 'new'");
        return NJS_ERROR;
    }

    decoder = njs_object_value_alloc(vm, NJS_OBJ_TYPE_TEXT_DECODER,
                                     sizeof(njs_encoding_decode_t), NULL);
    if (njs_slow_path(decoder == NULL)) {
        return NJS_ERROR;
    }

    data = (njs_encoding_decode_t *) ((uint8_t *) decoder
                                      + sizeof(njs_object_value_t));

    ret = njs_text_decoder_arg_encoding(vm, args, nargs, data);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_text_decoder_arg_options(vm, args, nargs, data);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    njs_utf8_decode_init(&data->ctx);

    njs_set_data(&decoder->value, data, NJS_DATA_TAG_TEXT_DECODER);
    njs_set_object_value(retval, decoder);

    return NJS_OK;
}


static njs_int_t
njs_text_decoder_arg_encoding(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_encoding_decode_t *data)
{
    njs_str_t             str;
    njs_int_t             ret;
    njs_value_t           *value;
    njs_encoding_label_t  *label;

    if (nargs < 2) {
        data->encoding = NJS_ENCODING_UTF8;
        return NJS_OK;
    }

    value = njs_argument(args, 1);

    if (njs_slow_path(!njs_is_string(value))) {
        ret = njs_value_to_string(vm, value, value);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    njs_string_get(vm, value, &str);

    for (label = &njs_encoding_labels[0]; label->name.length != 0; label++) {
        if (njs_strstr_eq(&str, &label->name)) {
            data->encoding = label->encoding;
            return NJS_OK;
        }
    }

    njs_range_error(vm, "The \"%V\" encoding is not supported", &str);

    return NJS_ERROR;
}


static njs_int_t
njs_text_decoder_arg_options(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_encoding_decode_t *data)
{
    njs_int_t    ret;
    njs_value_t  retval, *value;

    if (nargs < 3) {
        data->fatal = 0;
        data->ignore_bom = 0;

        return NJS_OK;
    }

    value = njs_argument(args, 2);

    if (njs_slow_path(!njs_is_object(value))) {
        njs_type_error(vm, "The \"options\" argument must be of type object");
        return NJS_ERROR;
    }

    ret = njs_value_property(vm, value, NJS_ATOM_STRING_fatal, &retval);
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    data->fatal = njs_bool(&retval);

    ret = njs_value_property(vm, value, NJS_ATOM_STRING_ignoreBOM, &retval);
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    data->ignore_bom = njs_bool(&retval);

    return NJS_OK;
}


static njs_int_t
njs_text_decoder_encoding(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    njs_encoding_decode_t  *data;

    if (njs_slow_path(!njs_is_object_data(value, NJS_DATA_TAG_TEXT_DECODER))) {
        njs_set_undefined(retval);
        return NJS_DECLINED;
    }

    data = njs_object_data(value);

    switch (data->encoding) {
    case NJS_ENCODING_UTF8:
        njs_atom_to_value(vm, retval, NJS_ATOM_STRING_utf_8);
        break;

    default:
        njs_type_error(vm, "unknown encoding");
        return NJS_ERROR;
    }

    return NJS_OK;
}


static njs_int_t
njs_text_decoder_fatal(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    njs_encoding_decode_t  *data;

    if (njs_slow_path(!njs_is_object_data(value, NJS_DATA_TAG_TEXT_DECODER))) {
        njs_set_undefined(retval);
        return NJS_DECLINED;
    }

    data = njs_object_data(value);

    njs_set_boolean(retval, data->fatal);

    return NJS_OK;
}


static njs_int_t
njs_text_decoder_ignore_bom(njs_vm_t *vm, njs_object_prop_t *prop,
    uint32_t unused, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    njs_encoding_decode_t  *data;

    if (njs_slow_path(!njs_is_object_data(value, NJS_DATA_TAG_TEXT_DECODER))) {
        njs_set_undefined(retval);
        return NJS_DECLINED;
    }

    data = njs_object_data(value);

    njs_set_boolean(retval, data->ignore_bom);

    return NJS_OK;
}


static njs_int_t
njs_text_decoder_decode(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    u_char                    *dst;
    size_t                    size;
    ssize_t                   length;
    njs_int_t                 ret;
    njs_bool_t                stream;
    njs_value_t               *this, *value, *options;
    const u_char              *start, *end;
    njs_unicode_decode_t      ctx;
    njs_encoding_decode_t     *data;
    const njs_typed_array_t   *array;
    const njs_array_buffer_t  *buffer;

    start = NULL;
    end = NULL;

    stream = 0;

    this = njs_argument(args, 0);

    if (njs_slow_path(!njs_is_object_data(this, NJS_DATA_TAG_TEXT_DECODER))) {
        njs_type_error(vm, "\"this\" is not a TextDecoder");
        return NJS_ERROR;
    }

    if (njs_fast_path(nargs > 1)) {
        value = njs_argument(args, 1);

        if (njs_is_typed_array(value)) {
            array = njs_typed_array(value);

            start = njs_typed_array_start(array);
            end = start + array->byte_length;

        } else if (njs_is_array_buffer(value)) {
            buffer = njs_array_buffer(value);

            start = buffer->u.u8;
            end = start + buffer->size;

        } else {
            njs_type_error(vm, "The \"input\" argument must be an instance "
                           "of TypedArray");
            return NJS_ERROR;
        }
    }

    if (nargs > 2) {
        options = njs_argument(args, 2);

        if (njs_slow_path(!njs_is_object(options))) {
            njs_type_error(vm, "The \"options\" argument must be "
                           "of type object");
            return NJS_ERROR;
        }

        ret = njs_value_property(vm, options, NJS_ATOM_STRING_stream, retval);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }

        stream = njs_bool(retval);
    }

    data = njs_object_data(this);

    ctx = data->ctx;

    /* Looking for BOM. */

    if (start != NULL && !data->ignore_bom) {
        start += njs_utf8_bom(start, end);
    }

    length = njs_utf8_stream_length(&ctx, start, end - start, !stream,
                                    data->fatal, &size);
    if (length == -1) {
        njs_type_error(vm, "The encoded data was not valid");
        return NJS_ERROR;
    }

    dst = njs_string_alloc(vm, retval, size, length);
    if (njs_slow_path(dst == NULL)) {
        return NJS_ERROR;
    }

    (void) njs_utf8_stream_encode(&data->ctx, start, end, dst, !stream, 0);

    if (!stream) {
        njs_utf8_decode_init(&data->ctx);
    }

    return NJS_OK;
}


static const njs_object_prop_init_t  njs_text_decoder_properties[] =
{
    NJS_DECLARE_PROP_HANDLER(STRING_constructor,
                             njs_object_prototype_create_constructor, 0,
                             NJS_OBJECT_PROP_VALUE_CW),

    NJS_DECLARE_PROP_HANDLER(STRING_encoding, njs_text_decoder_encoding, 0,
                             0),

    NJS_DECLARE_PROP_HANDLER(STRING_fatal, njs_text_decoder_fatal, 0, 0),

    NJS_DECLARE_PROP_HANDLER(STRING_ignoreBOM, njs_text_decoder_ignore_bom,
                             0, 0),

    NJS_DECLARE_PROP_NATIVE(STRING_decode, njs_text_decoder_decode, 0, 0),
};


const njs_object_init_t  njs_text_decoder_init = {
    njs_text_decoder_properties,
    njs_nitems(njs_text_decoder_properties),
};


static const njs_object_prop_init_t  njs_text_decoder_constructor_properties[] =
{
    NJS_DECLARE_PROP_LENGTH(0),

    NJS_DECLARE_PROP_NAME("TextDecoder"),

    NJS_DECLARE_PROP_HANDLER(STRING_prototype, njs_object_prototype_create,
                             0, 0),
};


const njs_object_init_t  njs_text_decoder_constructor_init = {
    njs_text_decoder_constructor_properties,
    njs_nitems(njs_text_decoder_constructor_properties),
};


const njs_object_type_init_t  njs_text_decoder_type_init = {
   .constructor = njs_native_ctor(njs_text_decoder_constructor, 0, 0),
   .prototype_props = &njs_text_decoder_init,
   .constructor_props = &njs_text_decoder_constructor_init,
   .prototype_value = { .object = { .type = NJS_OBJECT } },
};
