/*
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | https://www.php.net/license/3_01.txt                                 |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
   | Authors: Bogdan Ungureanu <bogdanungureanu21@gmail.com>              |
   +----------------------------------------------------------------------+
*/

#include "php.h"
#include "php_intl.h"
#include <unicode/ulistformatter.h>
#include "listformatter_class.h"
#include "listformatter_arginfo.h"
#include "intl_convert.h"

static zend_object_handlers listformatter_handlers;

static void listformatter_free_obj(zend_object *object)
{
    ListFormatter_object *obj = php_intl_listformatter_fetch_object(object);

    if( obj->lf_data.ulistfmt )
        ulistfmt_close( obj->lf_data.ulistfmt );

    obj->lf_data.ulistfmt = NULL;
    intl_error_reset( &obj->lf_data.error );

    zend_object_std_dtor(&obj->zo);
}

static zend_object *listformatter_create_object(zend_class_entry *class_type)
{
    ListFormatter_object *obj;
    obj = zend_object_alloc(sizeof(ListFormatter_object), class_type);

    obj->lf_data.ulistfmt = NULL;
    intl_error_reset( &obj->lf_data.error );

    zend_object_std_init(&obj->zo, class_type);
    object_properties_init(&obj->zo, class_type);
    obj->zo.handlers = &listformatter_handlers;
    return &obj->zo;
}

PHP_METHOD(IntlListFormatter, __construct)
{
    ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
    char* locale;
    size_t locale_len = 0;
    #if U_ICU_VERSION_MAJOR_NUM >= 67
        zend_long type = ULISTFMT_TYPE_AND;
        zend_long width = ULISTFMT_WIDTH_WIDE;
    #else
        zend_long type = INTL_LISTFORMATTER_FALLBACK_TYPE_AND;
        zend_long width = INTL_LISTFORMATTER_FALLBACK_WIDTH_WIDE;
    #endif
    ZEND_PARSE_PARAMETERS_START(1, 3)
        Z_PARAM_STRING(locale, locale_len)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(type)
        Z_PARAM_LONG(width)
    ZEND_PARSE_PARAMETERS_END();

    if(locale_len == 0) {
        locale = (char *)intl_locale_get_default();
    }

    if (locale_len > INTL_MAX_LOCALE_LEN) {
        zend_argument_value_error(1, "must be less than or equal to %d characters", INTL_MAX_LOCALE_LEN);
        RETURN_THROWS();
    }

    if (strlen(uloc_getISO3Language(locale)) == 0) {
        zend_argument_value_error(1, "\"%s\" is invalid", locale);
        RETURN_THROWS();
    }

    UErrorCode status = U_ZERO_ERROR;
    #if U_ICU_VERSION_MAJOR_NUM >= 67
        if (type != ULISTFMT_TYPE_AND && type != ULISTFMT_TYPE_OR && type != ULISTFMT_TYPE_UNITS) {
            zend_argument_value_error(2, "must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS");
            RETURN_THROWS();
        }
        
        if (width != ULISTFMT_WIDTH_WIDE && width != ULISTFMT_WIDTH_SHORT && width != ULISTFMT_WIDTH_NARROW) {
            zend_argument_value_error(3, "must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW");
            RETURN_THROWS();
        }

        LISTFORMATTER_OBJECT(obj) = ulistfmt_openForType(locale, type, width, &status);
    #else
        if (type != INTL_LISTFORMATTER_FALLBACK_TYPE_AND) {
            zend_argument_value_error(2, "contains an unsupported type. ICU 66 and below only support IntlListFormatter::TYPE_AND");
            RETURN_THROWS();
        }

        if (width != INTL_LISTFORMATTER_FALLBACK_WIDTH_WIDE) {
            zend_argument_value_error(3, "contains an unsupported width. ICU 66 and below only support IntlListFormatter::WIDTH_WIDE");
            RETURN_THROWS();
        }

        LISTFORMATTER_OBJECT(obj) = ulistfmt_open(locale, &status);
    #endif

    if (U_FAILURE(status)) {
        intl_error_set(NULL, status, "Constructor failed");
        zend_throw_exception(IntlException_ce_ptr, "Constructor failed", 0);
        RETURN_THROWS();
    }
}

PHP_METHOD(IntlListFormatter, format)
{
    ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
    HashTable *ht;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_ARRAY_HT(ht)
    ZEND_PARSE_PARAMETERS_END();

    uint32_t count = zend_hash_num_elements(ht);
    if (count == 0) {
        RETURN_EMPTY_STRING();
    }

    const UChar **items = (const UChar **)safe_emalloc(count, sizeof(const UChar *), 0);
    int32_t *itemLengths = (int32_t *)safe_emalloc(count, sizeof(int32_t), 0);
    uint32_t i = 0;
    zval *val;

    ZEND_HASH_FOREACH_VAL(ht, val) {
        zend_string *str_val, *tmp_str;
        
        str_val = zval_get_tmp_string(val, &tmp_str);
        
        // Convert PHP string to UTF-16
        UChar *ustr = NULL;
        int32_t ustr_len = 0;
        UErrorCode status = U_ZERO_ERROR;
        
        intl_convert_utf8_to_utf16(&ustr, &ustr_len, ZSTR_VAL(str_val), ZSTR_LEN(str_val), &status);
        zend_tmp_string_release(tmp_str);

        if (U_FAILURE(status)) {
            // We can't use goto cleanup because items and itemLengths are incompletely allocated
            for (uint32_t j = 0; j < i; j++) {
                efree((void *)items[j]);
            }
            efree(items);
            efree(itemLengths);
            intl_error_set(NULL, status, "Failed to convert string to UTF-16");
            RETURN_FALSE;
        }
        
        items[i] = ustr;
        itemLengths[i] = ustr_len;
        i++;
    } ZEND_HASH_FOREACH_END();

    UErrorCode status = U_ZERO_ERROR;
    int32_t resultLength;
    UChar *result = NULL;

    resultLength = ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, NULL, 0, &status);

    if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) {
        intl_error_set(NULL, status, "Failed to format list");
        RETVAL_FALSE;
        goto cleanup;
    }

    // Allocate buffer and try again
    status = U_ZERO_ERROR;
    result = (UChar *)safe_emalloc(resultLength + 1, sizeof(UChar), 0);
    ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, result, resultLength, &status);

    if (U_FAILURE(status)) {
        if (result) {
            efree(result);
        }
        intl_error_set(NULL, status, "Failed to format list");
        RETVAL_FALSE;
        goto cleanup;
    }

    // Convert result back to UTF-8
    zend_string *ret = intl_convert_utf16_to_utf8(result, resultLength, &status);
    efree(result);
    
    if (!ret) {
        intl_error_set(NULL, status, "Failed to convert result to UTF-8");
        RETVAL_FALSE;
    } else {
        RETVAL_NEW_STR(ret);
    }

cleanup:
    for (i = 0; i < count; i++) {
        efree((void *)items[i]);
    }
    efree(items);
    efree(itemLengths);
}

PHP_METHOD(IntlListFormatter, getErrorCode)
{
    ZEND_PARSE_PARAMETERS_NONE();

    ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);

    UErrorCode status = intl_error_get_code(LISTFORMATTER_ERROR_P(obj));

    RETURN_LONG(status);
}

PHP_METHOD(IntlListFormatter, getErrorMessage)
{
     ZEND_PARSE_PARAMETERS_NONE();

    ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);

    zend_string *message = intl_error_get_message(LISTFORMATTER_ERROR_P(obj));
    RETURN_STR(message);
}

void listformatter_register_class(void)
{
    zend_class_entry *class_entry = register_class_IntlListFormatter();
    class_entry->create_object = listformatter_create_object;
    
    memcpy(&listformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
    listformatter_handlers.offset = XtOffsetOf(ListFormatter_object, zo);
    listformatter_handlers.free_obj = listformatter_free_obj;
    listformatter_handlers.clone_obj = NULL;
}
