/*
 * tclIcu.c --
 *
 *	tclIcu.c implements various Tcl commands that make use of
 *	the ICU library if present on the system.
 *	(Adapted from tkIcu.c)
 *
 * Copyright © 2021 Jan Nijtmans
 * Copyright © 2024 Ashok P. Nadkarni
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tclInt.h"

typedef uint16_t UCharx;
typedef uint32_t UChar32x;

/*
 * Runtime linking of libicu.
 */
typedef enum UBreakIteratorTypex {
    UBRK_CHARACTERX = 0,
    UBRK_WORDX = 1
} UBreakIteratorTypex;

typedef enum UErrorCodex {
    U_STRING_NOT_TERMINATED_WARNING = -124,
    U_AMBIGUOUS_ALIAS_WARNING = -122,
    U_ZERO_ERRORZ = 0, /**< No error, no warning. */
    U_BUFFER_OVERFLOW_ERROR = 15,
} UErrorCodex;

#define U_SUCCESS(x) ((x)<=U_ZERO_ERRORZ)
#define U_FAILURE(x) ((x)>U_ZERO_ERRORZ)

typedef enum {
    UCNV_UNASSIGNED = 0,
    UCNV_ILLEGAL = 1,
    UCNV_IRREGULAR = 2,
    UCNV_RESET = 3,
    UCNV_CLOSE = 4,
    UCNV_CLONE = 5
} UConverterCallbackReasonx;

typedef enum UNormalizationCheckResultx {
    UNORM_NO,
    UNORM_YES,
    UNORM_MAYBE
} UNormalizationCheckResultx;

typedef struct UEnumeration UEnumeration;
typedef struct UCharsetDetector UCharsetDetector;
typedef struct UCharsetMatch UCharsetMatch;
typedef struct UBreakIterator UBreakIterator;
typedef struct UNormalizer2 UNormalizer2;
typedef struct UConverter UConverter;
typedef struct UConverterFromUnicodeArgs UConverterFromUnicodeArgs;
typedef struct UConverterToUnicodeArgs UConverterToUnicodeArgs;
typedef void   (*UConverterFromUCallback)(const void *context,
	UConverterFromUnicodeArgs *args, const UCharx *codeUnits,
	int32_t length, UChar32x codePoint, UConverterCallbackReasonx reason,
	UErrorCodex *pErrorCode);
typedef void   (*UConverterToUCallback)(const void *context,
	UConverterToUnicodeArgs *args, const char *codeUnits,
	int32_t length, UConverterCallbackReasonx reason,
	UErrorCodex *pErrorCode);
/*
 * Prototypes for ICU functions sorted by category.
 */
typedef void        (*fn_u_cleanup)(void);
typedef const char *(*fn_u_errorName)(UErrorCodex);
typedef UCharx *(*fn_u_strFromUTF32)(UCharx *dest, int32_t destCapacity,
	int32_t *pDestLength, const UChar32x *src, int32_t srcLength,
	UErrorCodex *pErrorCode);
typedef UCharx *(*fn_u_strFromUTF32WithSub)(UCharx *dest, int32_t destCapacity,
	int32_t *pDestLength, const UChar32x *src, int32_t srcLength,
	UChar32x subchar, int32_t *pNumSubstitutions, UErrorCodex *pErrorCode);
typedef UChar32x *(*fn_u_strToUTF32)(UChar32x *dest, int32_t destCapacity,
	int32_t *pDestLength, const UCharx *src, int32_t srcLength,
	UErrorCodex *pErrorCode);
typedef UChar32x *(*fn_u_strToUTF32WithSub)(UChar32x *dest,
	int32_t destCapacity, int32_t *pDestLength, const UCharx *src,
	int32_t srcLength, UChar32x subchar, int32_t *pNumSubstitutions,
	UErrorCodex *pErrorCode);

typedef void        (*fn_ucnv_close)(UConverter *);
typedef uint16_t    (*fn_ucnv_countAliases)(const char *, UErrorCodex *);
typedef int32_t     (*fn_ucnv_countAvailable)(void);
typedef int32_t     (*fn_ucnv_fromUChars)(UConverter *, char *dest,
	int32_t destCapacity, const UCharx *src, int32_t srcLen, UErrorCodex *);
typedef const char *(*fn_ucnv_getAlias)(const char *, uint16_t, UErrorCodex *);
typedef const char *(*fn_ucnv_getAvailableName)(int32_t);
typedef UConverter *(*fn_ucnv_open)(const char *converterName, UErrorCodex *);
typedef void        (*fn_ucnv_setFromUCallBack)(UConverter *,
	UConverterFromUCallback newAction, const void *newContext,
	UConverterFromUCallback *oldAction, const void **oldContext,
	UErrorCodex *err);
typedef void        (*fn_ucnv_setToUCallBack)(UConverter *,
	UConverterToUCallback newAction, const void *newContext,
	UConverterToUCallback *oldAction, const void **oldContext,
	UErrorCodex *err);
typedef int32_t     (*fn_ucnv_toUChars)(UConverter *, UCharx *dest,
	int32_t destCapacity, const char *src, int32_t srcLen, UErrorCodex *);
typedef UConverterFromUCallback fn_UCNV_FROM_U_CALLBACK_STOP;
typedef UConverterToUCallback   fn_UCNV_TO_U_CALLBACK_STOP;

typedef UBreakIterator *(*fn_ubrk_open)(
	UBreakIteratorTypex, const char *, const uint16_t *, int32_t,
	UErrorCodex *);
typedef void	(*fn_ubrk_close)(UBreakIterator *);
typedef int32_t	(*fn_ubrk_preceding)(UBreakIterator *, int32_t);
typedef int32_t	(*fn_ubrk_following)(UBreakIterator *, int32_t);
typedef int32_t	(*fn_ubrk_previous)(UBreakIterator *);
typedef int32_t	(*fn_ubrk_next)(UBreakIterator *);
typedef void	(*fn_ubrk_setText)(
	UBreakIterator *, const void *, int32_t, UErrorCodex *);

typedef UCharsetDetector * (*fn_ucsdet_open)(UErrorCodex *status);
typedef void               (*fn_ucsdet_close)(UCharsetDetector *ucsd);
typedef void               (*fn_ucsdet_setText)(UCharsetDetector *ucsd,
	const char *textIn, int32_t len, UErrorCodex *status);
typedef const char *       (*fn_ucsdet_getName)(
	const UCharsetMatch *ucsm, UErrorCodex *status);
typedef UEnumeration *     (*fn_ucsdet_getAllDetectableCharsets)(
	UCharsetDetector *ucsd, UErrorCodex *status);
typedef const UCharsetMatch *  (*fn_ucsdet_detect)(
	UCharsetDetector *ucsd, UErrorCodex *status);
typedef const UCharsetMatch ** (*fn_ucsdet_detectAll)(
	UCharsetDetector *ucsd, int32_t *matchesFound, UErrorCodex *status);

typedef void        (*fn_uenum_close)(UEnumeration *);
typedef int32_t     (*fn_uenum_count)(UEnumeration *, UErrorCodex *);
typedef const char *(*fn_uenum_next)(UEnumeration *, int32_t *, UErrorCodex *);

typedef UNormalizer2 *(*fn_unorm2_getNFCInstance)(UErrorCodex *);
typedef UNormalizer2 *(*fn_unorm2_getNFDInstance)(UErrorCodex *);
typedef UNormalizer2 *(*fn_unorm2_getNFKCInstance)(UErrorCodex *);
typedef UNormalizer2 *(*fn_unorm2_getNFKDInstance)(UErrorCodex *);
typedef int32_t (*fn_unorm2_normalize)(const UNormalizer2 *, const UCharx *,
	int32_t, UCharx *, int32_t, UErrorCodex *);

#define FIELD(name) fn_ ## name _ ## name

static struct {
    size_t nopen;		/* Total number of references to ALL libraries */
    /*
     * Depending on platform, ICU symbols may be distributed amongst
     * multiple libraries. For current functionality at most 2 needed.
     * Order of library loading is not guaranteed.
     */
    Tcl_LoadHandle      libs[2];

    FIELD(u_cleanup);
    FIELD(u_errorName);
    FIELD(u_strFromUTF32);
    FIELD(u_strFromUTF32WithSub);
    FIELD(u_strToUTF32);
    FIELD(u_strToUTF32WithSub);

    FIELD(ubrk_open);
    FIELD(ubrk_close);
    FIELD(ubrk_preceding);
    FIELD(ubrk_following);
    FIELD(ubrk_previous);
    FIELD(ubrk_next);
    FIELD(ubrk_setText);

    FIELD(ucnv_close);
    FIELD(ucnv_countAliases);
    FIELD(ucnv_countAvailable);
    FIELD(ucnv_fromUChars);
    FIELD(ucnv_getAlias);
    FIELD(ucnv_getAvailableName);
    FIELD(ucnv_open);
    FIELD(ucnv_setFromUCallBack);
    FIELD(ucnv_setToUCallBack);
    FIELD(ucnv_toUChars);
    FIELD(UCNV_FROM_U_CALLBACK_STOP);
    FIELD(UCNV_TO_U_CALLBACK_STOP);

    FIELD(ucsdet_close);
    FIELD(ucsdet_detect);
    FIELD(ucsdet_detectAll);
    FIELD(ucsdet_getAllDetectableCharsets);
    FIELD(ucsdet_getName);
    FIELD(ucsdet_open);
    FIELD(ucsdet_setText);

    FIELD(uenum_close);
    FIELD(uenum_count);
    FIELD(uenum_next);
    FIELD(unorm2_getNFCInstance);
    FIELD(unorm2_getNFDInstance);
    FIELD(unorm2_getNFKCInstance);
    FIELD(unorm2_getNFKDInstance);
    FIELD(unorm2_normalize);
} icu_fns = {
    0,    {NULL, NULL}, /* Reference count, library handles */
    NULL, NULL, NULL, NULL, NULL, NULL,			    /* u_* */
    NULL, NULL, NULL, NULL, NULL, NULL, NULL,		    /* ubrk* */
    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,   /* ucnv_* .. */
    NULL, NULL, NULL,					    /* .. ucnv_ */
    NULL, NULL, NULL, NULL, NULL, NULL, NULL,		    /* ucsdet* */
    NULL, NULL, NULL,					    /* uenum_* */
    NULL, NULL, NULL, NULL, NULL,			    /* unorm2_* */
};

#define u_cleanup        icu_fns._u_cleanup
#define u_errorName      icu_fns._u_errorName
#define u_strFromUTF32   icu_fns._u_strFromUTF32
#define u_strFromUTF32WithSub icu_fns._u_strFromUTF32WithSub
#define u_strToUTF32          icu_fns._u_strToUTF32
#define u_strToUTF32WithSub   icu_fns._u_strToUTF32WithSub

#define ubrk_open        icu_fns._ubrk_open
#define ubrk_close       icu_fns._ubrk_close
#define ubrk_preceding   icu_fns._ubrk_preceding
#define ubrk_following   icu_fns._ubrk_following
#define ubrk_previous    icu_fns._ubrk_previous
#define ubrk_next        icu_fns._ubrk_next
#define ubrk_setText     icu_fns._ubrk_setText

#define ucnv_close            icu_fns._ucnv_close
#define ucnv_countAliases     icu_fns._ucnv_countAliases
#define ucnv_countAvailable   icu_fns._ucnv_countAvailable
#define ucnv_fromUChars       icu_fns._ucnv_fromUChars
#define ucnv_getAlias         icu_fns._ucnv_getAlias
#define ucnv_getAvailableName icu_fns._ucnv_getAvailableName
#define ucnv_open             icu_fns._ucnv_open
#define ucnv_setFromUCallBack icu_fns._ucnv_setFromUCallBack
#define ucnv_setToUCallBack   icu_fns._ucnv_setToUCallBack
#define ucnv_toUChars         icu_fns._ucnv_toUChars
#define UCNV_FROM_U_CALLBACK_STOP icu_fns._UCNV_FROM_U_CALLBACK_STOP
#define UCNV_TO_U_CALLBACK_STOP   icu_fns._UCNV_TO_U_CALLBACK_STOP

#define ucsdet_close     icu_fns._ucsdet_close
#define ucsdet_detect    icu_fns._ucsdet_detect
#define ucsdet_detectAll icu_fns._ucsdet_detectAll
#define ucsdet_getAllDetectableCharsets icu_fns._ucsdet_getAllDetectableCharsets
#define ucsdet_getName   icu_fns._ucsdet_getName
#define ucsdet_open      icu_fns._ucsdet_open
#define ucsdet_setText   icu_fns._ucsdet_setText

#define uenum_next       icu_fns._uenum_next
#define uenum_close      icu_fns._uenum_close
#define uenum_count      icu_fns._uenum_count

#define unorm2_getNFCInstance  icu_fns._unorm2_getNFCInstance
#define unorm2_getNFDInstance  icu_fns._unorm2_getNFDInstance
#define unorm2_getNFKCInstance icu_fns._unorm2_getNFKCInstance
#define unorm2_getNFKDInstance icu_fns._unorm2_getNFKDInstance
#define unorm2_normalize       icu_fns._unorm2_normalize

TCL_DECLARE_MUTEX(icu_mutex);

/* Options used by multiple normalization functions */
static const char *const normalizationForms[] = {"nfc", "nfd", "nfkc", "nfkd", NULL};
typedef enum { MODE_NFC, MODE_NFD, MODE_NFKC, MODE_NFKD } NormalizationMode;

/* Error handlers. */

static int
FunctionNotAvailableError(
    Tcl_Interp *interp)
{
    if (interp) {
	Tcl_SetObjResult(interp, Tcl_NewStringObj(
		"ICU function not available", TCL_AUTO_LENGTH));
	Tcl_SetErrorCode(interp, "TCL", "ICU", "UNSUPPORTED_OP", (char *)NULL);
    }
    return TCL_ERROR;
}

static int
IcuError(
    Tcl_Interp *interp,
    const char *message,
    UErrorCodex code)
{
    if (interp) {
	const char *codeMessage = NULL;
	if (u_errorName) {
	    codeMessage = u_errorName(code);
	}
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"%s%sICU error (%d): %s",
		message ? message : "",
		message ? ". " : "",
		code,
		codeMessage ? codeMessage : ""));
	Tcl_SetErrorCode(interp, "TCL", "ICU", codeMessage, (char *)NULL);
    }
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * DetectEncoding --
 *
 *	Detect the likely encoding of the string encoded in the given byte
 *	array.
 *
 *----------------------------------------------------------------------
 */
static int
DetectEncoding(
    Tcl_Interp *interp,
    Tcl_Obj *objPtr,
    int all)
{
    Tcl_Size len;
    const char *bytes;
    const UCharsetMatch *match;
    const UCharsetMatch **matches;
    int nmatches;
    int ret;

    // Confirm we have the profile of functions we need.
    if (ucsdet_open == NULL ||
	    ucsdet_setText == NULL ||
	    ucsdet_detect == NULL ||
	    ucsdet_detectAll == NULL ||
	    ucsdet_getName == NULL ||
	    ucsdet_close == NULL) {
	return FunctionNotAvailableError(interp);
    }

    bytes = (char *)Tcl_GetBytesFromObj(interp, objPtr, &len);
    if (bytes == NULL) {
	return TCL_ERROR;
    }
    if (len > INT_MAX) {
	Tcl_SetObjResult(interp, Tcl_NewStringObj(
		"Max length supported by ICU exceeded.", TCL_INDEX_NONE));
	Tcl_SetErrorCode(interp, "TCL", "OPERATION", "LENGTH", (char *)NULL);
	return TCL_ERROR;
    }
    UErrorCodex status = U_ZERO_ERRORZ;

    UCharsetDetector* csd = ucsdet_open(&status);
    if (U_FAILURE(status)) {
	return IcuError(interp, "Could not open charset detector", status);
    }

    ucsdet_setText(csd, bytes, (int)len, &status);
    if (U_FAILURE(status)) {
	IcuError(interp, "Could not set detection text", status);
	ucsdet_close(csd);
	return TCL_ERROR;
    }

    if (all) {
	matches = ucsdet_detectAll(csd, &nmatches, &status);
    } else {
	match = ucsdet_detect(csd, &status);
	matches = &match;
	nmatches = match ? 1 : 0;
    }

    if (U_FAILURE(status) || nmatches == 0) {
	ret = IcuError(interp, "Could not detect character set", status);
    } else {
	int i;
	Tcl_Obj *resultObj = Tcl_NewListObj(nmatches, NULL);

	for (i = 0; i < nmatches; ++i) {
	    const char *name = ucsdet_getName(matches[i], &status);
	    if (U_FAILURE(status) || name == NULL) {
		name = "unknown";
		status = U_ZERO_ERRORZ; /* Reset on failure */
	    }
	    Tcl_ListObjAppendElement(
		    NULL, resultObj, Tcl_NewStringObj(name, TCL_AUTO_LENGTH));
	}
	Tcl_SetObjResult(interp, resultObj);
	ret = TCL_OK;
    }

    ucsdet_close(csd);
    return ret;
}

static int
DetectableEncodings(
    Tcl_Interp *interp)
{
    // Confirm we have the profile of functions we need.
    if (ucsdet_open == NULL ||
	    ucsdet_getAllDetectableCharsets == NULL ||
	    ucsdet_close == NULL ||
	    uenum_next == NULL ||
	    uenum_count == NULL ||
	    uenum_close == NULL) {
	return FunctionNotAvailableError(interp);
    }
    UErrorCodex status = U_ZERO_ERRORZ;
    UCharsetDetector *csd = ucsdet_open(&status);

    if (U_FAILURE(status)) {
	return IcuError(interp, "Could not open charset detector", status);
    }

    int ret;
    UEnumeration *enumerator = ucsdet_getAllDetectableCharsets(csd, &status);
    if (U_FAILURE(status) || enumerator == NULL) {
	IcuError(interp, "Could not get list of detectable encodings", status);
	ret = TCL_ERROR;
    } else {
	int32_t count = uenum_count(enumerator, &status);

	if (U_FAILURE(status)) {
	    IcuError(interp, "Could not get charset enumerator count", status);
	    ret = TCL_ERROR;
	} else {
	    int i;
	    Tcl_Obj *resultObj = Tcl_NewListObj(0, NULL);

	    for (i = 0; i < count; ++i) {
		const char *name;
		int32_t len;

		name = uenum_next(enumerator, &len, &status);
		if (name == NULL || U_FAILURE(status)) {
		    name = "unknown";
		    len = 7;
		    status = U_ZERO_ERRORZ; /* Reset on error */
		}
		Tcl_ListObjAppendElement(
			NULL, resultObj, Tcl_NewStringObj(name, len));
	    }
	    Tcl_SetObjResult(interp, resultObj);
	    ret = TCL_OK;
	}
	uenum_close(enumerator);
    }

    ucsdet_close(csd);
    return ret;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuObjToUCharDString --
 *
 *	Encodes a Tcl_Obj value in ICU UChars and stores in dsPtr.
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	*dsPtr should be cleared by caller only if return code is TCL_OK.
 *
 *------------------------------------------------------------------------
 */
static int
IcuObjToUCharDString(
    Tcl_Interp *interp,
    Tcl_Obj *objPtr,
    int strict,
    Tcl_DString *dsPtr)
{
    Tcl_Encoding encoding;

    /*
     * TODO - not the most efficient to get an encoding every time.
     * However, we cannot use Tcl_UtfToChar16DString as that blithely
     * ignores invalid or ill-formed UTF8 strings.
     */
    encoding = Tcl_GetEncoding(interp, "utf-16");
    if (encoding == NULL) {
	return TCL_ERROR;
    }

    int result;
    char *s;
    Tcl_Size len;
    s = Tcl_GetStringFromObj(objPtr, &len);
    result = Tcl_UtfToExternalDStringEx(interp, encoding, s, len,
	    strict ? TCL_ENCODING_PROFILE_STRICT : TCL_ENCODING_PROFILE_REPLACE,
	    dsPtr, NULL);
    if (result != TCL_OK) {
	Tcl_DStringFree(dsPtr); /* Must be done on error */
	/* TCL_CONVER_* errors -> TCL_ERROR */
	result = TCL_ERROR;
    }

    Tcl_FreeEncoding(encoding);
    return result;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuObjFromUCharDString --
 *
 *	Stores a Tcl_Obj value by decoding ICU UChars in dsPtr.
 *
 * Results:
 *	Return Tcl_Obj or NULL on error.
 *
 * Side effects:
 *	None.
 *
 *------------------------------------------------------------------------
 */
static Tcl_Obj *
IcuObjFromUCharDString(
    Tcl_Interp *interp,
    Tcl_DString *dsPtr,
    int strict)
{
    Tcl_Encoding encoding;

    /*
     * TODO - not the most efficient to get an encoding every time.
     * However, we cannot use Tcl_UtfToChar16DString as that blithely
     * ignores invalid or ill-formed UTF8 strings.
     */
    encoding = Tcl_GetEncoding(interp, "utf-16");
    if (encoding == NULL) {
	return NULL;
    }
    Tcl_Obj *objPtr = NULL;
    char *s = Tcl_DStringValue(dsPtr);
    Tcl_Size len = Tcl_DStringLength(dsPtr);
    Tcl_DString dsOut;
    int result;
    result  = Tcl_ExternalToUtfDStringEx(interp, encoding, s, len,
	    strict ? TCL_ENCODING_PROFILE_STRICT : TCL_ENCODING_PROFILE_REPLACE,
	    &dsOut, NULL);

    if (result == TCL_OK) {
	objPtr = Tcl_DStringToObj(&dsOut); /* Clears dsPtr! */
    }

    Tcl_FreeEncoding(encoding);
    return objPtr;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuDetectObjCmd --
 *
 *	Implements the Tcl command ::tcl::unsupported::icu::detect.
 *	  ::tcl::unsupported::icu::detect - returns names of all detectable encodings
 *	  ::tcl::unsupported::icu::detect BYTES ?-all? - return detected encoding(s)
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds result or error message.
 *
 *------------------------------------------------------------------------
 */
static int
IcuDetectObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,
    Tcl_Size objc,
    Tcl_Obj *const *objv)
{
    if (objc > 3) {
	Tcl_WrongNumArgs(interp, 1 , objv, "?bytes ?-all??");
	return TCL_ERROR;
    }

    if (objc == 1) {
	return DetectableEncodings(interp);
    }

    int all = 0;
    if (objc == 3) {
	if (strcmp("-all", Tcl_GetString(objv[2]))) {
	    Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		    "Invalid option %s, must be \"-all\"",
		    Tcl_GetString(objv[2])));
	    Tcl_SetErrorCode(interp, "TCL", "OPERATION", "BADARG", (char *)NULL);
	    return TCL_ERROR;
	}
	all = 1;
    }

    return DetectEncoding(interp, objv[1], all);
}

/*
 *------------------------------------------------------------------------
 *
 * IcuConverterNamesObjCmd --
 *
 *	Sets interp result to list of available ICU converters.
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds list of converter names.
 *
 *------------------------------------------------------------------------
 */
static int
IcuConverterNamesObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{

    if (objc != 1) {
	Tcl_WrongNumArgs(interp, 1 , objv, "");
	return TCL_ERROR;
    }
    if (ucnv_countAvailable == NULL || ucnv_getAvailableName == NULL) {
	return FunctionNotAvailableError(interp);
    }

    int32_t count = ucnv_countAvailable();
    if (count <= 0) {
	return TCL_OK;
    }
    Tcl_Obj *resultObj = Tcl_NewListObj(count, NULL);
    int32_t i;

    for (i = 0; i < count; ++i) {
	const char *name = ucnv_getAvailableName(i);
	if (name) {
	    Tcl_ListObjAppendElement(NULL, resultObj,
		    Tcl_NewStringObj(name, TCL_AUTO_LENGTH));
	}
    }
    Tcl_SetObjResult(interp, resultObj);
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuConverterAliasesObjCmd --
 *
 *	Sets interp result to list of available ICU converters.
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds list of converter names.
 *
 *------------------------------------------------------------------------
 */
static int
IcuConverterAliasesObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{
    if (objc != 2) {
	Tcl_WrongNumArgs(interp, 1 , objv, "convertername");
	return TCL_ERROR;
    }
    if (ucnv_countAliases == NULL || ucnv_getAlias == NULL) {
	return FunctionNotAvailableError(interp);
    }

    const char *name = Tcl_GetString(objv[1]);
    UErrorCodex status = U_ZERO_ERRORZ;
    uint16_t count = ucnv_countAliases(name, &status);
    if (status != U_AMBIGUOUS_ALIAS_WARNING && U_FAILURE(status)) {
	return IcuError(interp, "Could not get aliases", status);
    }
    if (count <= 0) {
	return TCL_OK;
    }

    Tcl_Obj *resultObj = Tcl_NewListObj(count, NULL);
    uint16_t i;

    for (i = 0; i < count; ++i) {
	status = U_ZERO_ERRORZ; /* Reset in case U_AMBIGUOUS_ALIAS_WARNING */
	const char *aliasName = ucnv_getAlias(name, i, &status);

	if (status != U_AMBIGUOUS_ALIAS_WARNING && U_FAILURE(status)) {
	    status = U_ZERO_ERRORZ; /* Reset error for next iteration */
	    continue;
	}
	if (aliasName) {
	    Tcl_ListObjAppendElement(NULL, resultObj,
		    Tcl_NewStringObj(aliasName, TCL_AUTO_LENGTH));
	}
    }
    Tcl_SetObjResult(interp, resultObj);
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuConverttoDString --
 *
 *	Converts a string in ICU default encoding to the specified encoding.
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	On success, encoded string is stored in output dsOutPtr
 *
 *------------------------------------------------------------------------
 */
static int
IcuConverttoDString(
    Tcl_Interp *interp,
    Tcl_DString *dsInPtr,	/* Input UTF16 */
    const char *icuEncName,
    int strict,
    Tcl_DString *dsOutPtr)	/* Output encoded string. */
{
    if (ucnv_open == NULL || ucnv_close == NULL ||
	    ucnv_fromUChars == NULL || UCNV_FROM_U_CALLBACK_STOP == NULL) {
	return FunctionNotAvailableError(interp);
    }

    UErrorCodex status = U_ZERO_ERRORZ;
    UConverter *ucnvPtr = ucnv_open(icuEncName, &status);
    if (ucnvPtr == NULL) {
	return IcuError(interp, "Could not get encoding converter", status);
    }
    if (strict) {
	ucnv_setFromUCallBack(ucnvPtr, UCNV_FROM_U_CALLBACK_STOP, NULL, NULL, NULL, &status);
	if (U_FAILURE(status)) {
	    /* TODO - use ucnv_getInvalidUChars to retrieve failing chars */
	    ucnv_close(ucnvPtr);
	    return IcuError(interp, "Could not set conversion callback", status);
	}
    }

    UCharx *utf16 = (UCharx *) Tcl_DStringValue(dsInPtr);
    Tcl_Size utf16len = Tcl_DStringLength(dsInPtr) / sizeof(UCharx);
    Tcl_Size dstLen, dstCapacity;
    if (utf16len > INT_MAX) {
	Tcl_SetObjResult(interp, Tcl_NewStringObj(
		"Max length supported by ICU exceeded.", TCL_INDEX_NONE));
	Tcl_SetErrorCode(interp, "TCL", "OPERATION", "LENGTH", (char *)NULL);
	return TCL_ERROR;
    }

    dstCapacity = utf16len;
    Tcl_DStringInit(dsOutPtr);
    Tcl_DStringSetLength(dsOutPtr, dstCapacity);
    dstLen = ucnv_fromUChars(ucnvPtr, Tcl_DStringValue(dsOutPtr), (int)dstCapacity,
	    utf16, (int)utf16len, &status);
    if (U_FAILURE(status)) {
	switch (status) {
	case U_STRING_NOT_TERMINATED_WARNING:
	    break; /* We don't care */
	case U_BUFFER_OVERFLOW_ERROR:
	    Tcl_DStringSetLength(dsOutPtr, (int)dstLen);
	    status = U_ZERO_ERRORZ; /* Reset before call */
	    dstLen = ucnv_fromUChars(ucnvPtr, Tcl_DStringValue(dsOutPtr), (int)dstLen,
		    utf16, (int)utf16len, &status);
	    if (U_SUCCESS(status)) {
		break;
	    }
	    TCL_FALLTHROUGH();
	default:
	    Tcl_DStringFree(dsOutPtr);
	    ucnv_close(ucnvPtr);
	    return IcuError(interp, "ICU error while encoding", status);
	}
    }
    Tcl_DStringSetLength(dsOutPtr, dstLen);
    ucnv_close(ucnvPtr);
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuBytesToUCharDString --
 *
 *	Converts encoded bytes to ICU UChars in a Tcl_DString
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	On success, encoded string is stored in output dsOutPtr
 *
 *------------------------------------------------------------------------
 */
static int
IcuBytesToUCharDString(
    Tcl_Interp *interp,
    const unsigned char *bytes,
    Tcl_Size nbytes,
    const char *icuEncName,
    int strict,
    Tcl_DString *dsOutPtr)	/* Output UChar string. */
{
    if (ucnv_open == NULL || ucnv_close == NULL ||
	    ucnv_toUChars == NULL || UCNV_TO_U_CALLBACK_STOP == NULL) {
	return FunctionNotAvailableError(interp);
    }

    if (nbytes > INT_MAX) {
	Tcl_SetObjResult(interp, Tcl_NewStringObj(
		"Max length supported by ICU exceeded.", TCL_INDEX_NONE));
	Tcl_SetErrorCode(interp, "TCL", "OPERATION", "LENGTH", (char *)NULL);
	return TCL_ERROR;
    }

    UErrorCodex status = U_ZERO_ERRORZ;
    UConverter *ucnvPtr = ucnv_open(icuEncName, &status);
    if (ucnvPtr == NULL) {
	return IcuError(interp, "Could not get encoding converter", status);
    }
    if (strict) {
	ucnv_setToUCallBack(ucnvPtr, UCNV_TO_U_CALLBACK_STOP, NULL, NULL, NULL, &status);
	if (U_FAILURE(status)) {
	    /* TODO - use ucnv_getInvalidUChars to retrieve failing chars */
	    ucnv_close(ucnvPtr);
	    return IcuError(interp, "Could not set conversion callback", status);
	}
    }

    int dstLen;
    int dstCapacity = (int)nbytes; /* In UChar's */
    Tcl_DStringInit(dsOutPtr);
    Tcl_DStringSetLength(dsOutPtr, dstCapacity);
    dstLen = ucnv_toUChars(ucnvPtr, (UCharx *)Tcl_DStringValue(dsOutPtr), dstCapacity,
	    (const char *)bytes, (int)nbytes, &status);
    if (U_FAILURE(status)) {
	switch (status) {
	case U_STRING_NOT_TERMINATED_WARNING:
	    break; /* We don't care */
	case U_BUFFER_OVERFLOW_ERROR:
	    dstCapacity = sizeof(UCharx) * dstLen;
	    Tcl_DStringSetLength(dsOutPtr, dstCapacity);
	    status = U_ZERO_ERRORZ; /* Reset before call */
	    dstLen = ucnv_toUChars(ucnvPtr, (UCharx *)Tcl_DStringValue(dsOutPtr), dstCapacity,
		    (const char *)bytes, (int)nbytes, &status);
	    if (U_SUCCESS(status)) {
		break;
	    }
	    TCL_FALLTHROUGH();
	default:
	    Tcl_DStringFree(dsOutPtr);
	    ucnv_close(ucnvPtr);
	    return IcuError(interp, "ICU error while decoding", status);
	}
    }
    Tcl_DStringSetLength(dsOutPtr, sizeof(UCharx)*dstLen);
    ucnv_close(ucnvPtr);
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuNormalizeUCharDString --
 *
 *	Normalizes the UTF-16 encoded data
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Normalized data is stored in dsOutPtr which should only be
 *	Tcl_DStringFree-ed if return code is TCL_OK.
 *
 *------------------------------------------------------------------------
 */
static int
IcuNormalizeUCharDString(
    Tcl_Interp *interp,
    Tcl_DString *dsInPtr,	/* Input UTF16 */
    NormalizationMode mode,
    Tcl_DString *dsOutPtr)	/* Output normalized UTF16. */
{
    typedef UNormalizer2 *(*normFn)(UErrorCodex *);
    normFn fn = NULL;

    switch (mode) {
    case MODE_NFC:
	fn = unorm2_getNFCInstance;
	break;
    case MODE_NFD:
	fn = unorm2_getNFDInstance;
	break;
    case MODE_NFKC:
	fn = unorm2_getNFKCInstance;
	break;
    case MODE_NFKD:
	fn = unorm2_getNFKDInstance;
	break;
    }
    if (fn == NULL || unorm2_normalize == NULL) {
	return FunctionNotAvailableError(interp);
    }

    UErrorCodex status = U_ZERO_ERRORZ;
    UNormalizer2 *normalizer = fn(&status);
    if (U_FAILURE(status)) {
	return IcuError(interp, "Could not get ICU normalizer", status);
    }

    UCharx *utf16;
    Tcl_Size utf16len;
    UCharx *normPtr;
    int32_t normLen;

    utf16 = (UCharx *) Tcl_DStringValue(dsInPtr);
    utf16len = Tcl_DStringLength(dsInPtr) / sizeof(UCharx);
    if (utf16len > INT_MAX) {
	Tcl_SetObjResult(interp, Tcl_NewStringObj(
		"Max length supported by ICU exceeded.", TCL_INDEX_NONE));
	Tcl_SetErrorCode(interp, "TCL", "OPERATION", "LENGTH", (char *)NULL);
	return TCL_ERROR;
    }
    Tcl_DStringInit(dsOutPtr);
    Tcl_DStringSetLength(dsOutPtr, utf16len * sizeof(UCharx));
    normPtr = (UCharx *) Tcl_DStringValue(dsOutPtr);

    normLen = unorm2_normalize(
	normalizer, utf16, (int)utf16len, normPtr, (int)utf16len, &status);
    if (U_FAILURE(status)) {
	switch (status) {
	case U_STRING_NOT_TERMINATED_WARNING:
	    /* No problem, don't need it terminated */
	    break;
	case U_BUFFER_OVERFLOW_ERROR:
	    /* Expand buffer */
	    Tcl_DStringSetLength(dsOutPtr, normLen * sizeof(UCharx));
	    normPtr = (UCharx *) Tcl_DStringValue(dsOutPtr);
	    status = U_ZERO_ERRORZ; /* Need to clear error! */
	    normLen = unorm2_normalize(normalizer,
		    utf16, (int)utf16len, normPtr, normLen, &status);
	    if (U_SUCCESS(status)) {
		break;
	    }
	    TCL_FALLTHROUGH();
	default:
	    Tcl_DStringFree(dsOutPtr);
	    return IcuError(interp, "String normalization failed", status);
	}
    }

    Tcl_DStringSetLength(dsOutPtr, normLen * sizeof(UCharx));
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * IcuParseConvertOptions --
 *
 *	Common function for parsing convert options.
 *
 *----------------------------------------------------------------------
 */
static int
IcuParseConvertOptions(
    Tcl_Interp *interp,
    Tcl_Size objc,
    Tcl_Obj *const *objv,
    int *strictPtr,
    Tcl_Obj **failindexVarPtr)
{
    if (objc < 3) {
	Tcl_WrongNumArgs(interp, 1, objv, "?-profile PROFILE? ICUENCNAME STRING");
	return TCL_ERROR;
    }
    objc -= 2; /* truncate fixed arguments */

    /* Use GetIndexFromObj for option parsing so -failindex can be added later */

    static const char *const optNames[] = {"-profile", "-failindex", NULL};
    enum { OPT_PROFILE, OPT_FAILINDEX } opt;
    int i;
    int strict = 1;
    for (i = 1; i < objc; ++i) {
	if (Tcl_GetIndexFromObj(interp,
		objv[i], optNames, "option", 0, &opt) != TCL_OK) {
	    return TCL_ERROR;
	}
	++i;
	if (i == objc) {
	    Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		    "Missing value for option %s.",
		    Tcl_GetString(objv[i - 1])));
	    Tcl_SetErrorCode(interp, "TCL", "OPERATION", "NOARG", (char *)NULL);
	    return TCL_ERROR;
	}
	const char *s = Tcl_GetString(objv[i]);
	switch (opt) {
	case OPT_PROFILE:
	    if (!strcmp(s, "replace")) {
		strict = 0;
	    } else if (strcmp(s, "strict")) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf(
			"Invalid value \"%s\" supplied for option"
			" \"-profile\". Must be \"strict\" or \"replace\".",
			s));
		Tcl_SetErrorCode(interp, "TCL", "VALUE", "PROFILE", (char *)NULL);
		return TCL_ERROR;
	    }
	    break;
	case OPT_FAILINDEX:
	    /* TBD */
	    Tcl_SetObjResult(interp, Tcl_NewStringObj(
		    "Option -failindex not implemented.", TCL_INDEX_NONE));
	    Tcl_SetErrorCode(interp, "TCL", "OPERATION", "UNIMPLEMENTED", (char *)NULL);
	    return TCL_ERROR;
	default:
	    TCL_UNREACHABLE();
	}
    }
    *strictPtr = strict;
    *failindexVarPtr = NULL;
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuConvertfromObjCmd --
 *
 *	Implements the Tcl command "icu convertfrom"
 *	    icu convertfrom ?-profile replace|strict? encoding string
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds result or error message.
 *
 *------------------------------------------------------------------------
 */
static int
IcuConvertfromObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{
    int strict;
    Tcl_Obj *failindexVar;

    if (IcuParseConvertOptions(interp, objc, objv, &strict, &failindexVar) != TCL_OK) {
	return TCL_ERROR;
    }

    Tcl_Size nbytes;
    const unsigned char *bytes = Tcl_GetBytesFromObj(interp, objv[objc-1], &nbytes);
    if (bytes == NULL) {
	return TCL_ERROR;
    }

    Tcl_DString ds;
    if (IcuBytesToUCharDString(interp, bytes, nbytes,
	    Tcl_GetString(objv[objc-2]), strict, &ds) != TCL_OK) {
	return TCL_ERROR;
    }
    Tcl_Obj *resultObj = IcuObjFromUCharDString(interp, &ds, strict);
    if (resultObj) {
	Tcl_SetObjResult(interp, resultObj);
	return TCL_OK;
    } else {
	return TCL_ERROR;
    }
}

/*
 *------------------------------------------------------------------------
 *
 * IcuConverttoObjCmd --
 *
 *	Implements the Tcl command "icu convertto"
 *	    icu convertto ?-profile replace|strict? encoding string
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds result or error message.
 *
 *------------------------------------------------------------------------
 */
static int
IcuConverttoObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{
    int strict;
    Tcl_Obj *failindexVar;

    if (IcuParseConvertOptions(interp, objc, objv, &strict, &failindexVar) != TCL_OK) {
	return TCL_ERROR;
    }

    Tcl_DString dsIn;
    Tcl_DString dsOut;
    if (IcuObjToUCharDString(interp, objv[objc - 1], strict, &dsIn) != TCL_OK ||
	    IcuConverttoDString(interp, &dsIn,
		    Tcl_GetString(objv[objc-2]), strict, &dsOut) != TCL_OK) {
	return TCL_ERROR;
    }
    Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(
	    (unsigned char *)Tcl_DStringValue(&dsOut),
	    Tcl_DStringLength(&dsOut)));
    Tcl_DStringFree(&dsOut);
    return TCL_OK;
}

/*
 *------------------------------------------------------------------------
 *
 * IcuNormalizeObjCmd --
 *
 *	Implements the Tcl command "icu normalize"
 *	    icu normalize ?-profile replace|strict? ?-mode nfc|nfd|nfkc|nfkd? string
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds result or error message.
 *
 *------------------------------------------------------------------------
 */
static int
IcuNormalizeObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{
    static const char *const optNames[] = {"-profile", "-mode", NULL};
    enum { OPT_PROFILE, OPT_MODE } opt;

    if (objc < 2) {
	Tcl_WrongNumArgs(interp, 1, objv, "?-profile PROFILE? ?-mode MODE? STRING");
	return TCL_ERROR;
    }

    int i;
    int strict = 1;
    NormalizationMode mode = MODE_NFC;
    for (i = 1; i < objc - 1; ++i) {
	if (Tcl_GetIndexFromObj(interp,
		objv[i], optNames, "option", 0, &opt) != TCL_OK) {
	    return TCL_ERROR;
	}
	++i;
	if (i == (objc-1)) {
	    Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		    "Missing value for option %s.",
		    Tcl_GetString(objv[i - 1])));
	    Tcl_SetErrorCode(interp, "TCL", "OPERATION", "NOARG", (char *)NULL);
	    return TCL_ERROR;
	}
	const char *s = Tcl_GetString(objv[i]);
	switch (opt) {
	case OPT_PROFILE:
	    if (!strcmp(s, "replace")) {
		strict = 0;
	    } else if (strcmp(s, "strict")) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf(
			"Invalid value \"%s\" supplied for option \"-profile\". "
			"Must be \"strict\" or \"replace\".",
			s));
		Tcl_SetErrorCode(interp, "TCL", "VALUE", "PROFILE", (char *)NULL);
		return TCL_ERROR;
	    }
	    break;
	case OPT_MODE:
	    if (Tcl_GetIndexFromObj(interp, objv[i], normalizationForms,
		    "normalization mode", 0, &mode) != TCL_OK) {
		return TCL_ERROR;
	    }
	    break;
	default:
	    TCL_UNREACHABLE();
	}
    }

    Tcl_DString dsIn;
    Tcl_DString dsNorm;
    if (IcuObjToUCharDString(interp, objv[objc - 1], strict, &dsIn) != TCL_OK ||
	    IcuNormalizeUCharDString(interp, &dsIn, mode, &dsNorm) != TCL_OK) {
	return TCL_ERROR;
    }
    Tcl_DStringFree(&dsIn);
    Tcl_Obj *objPtr = IcuObjFromUCharDString(interp, &dsNorm, strict);
    Tcl_DStringFree(&dsNorm);
    if (objPtr) {
	Tcl_SetObjResult(interp, objPtr);
	return TCL_OK;
    } else {
	return TCL_ERROR;
    }
}

/*
 *------------------------------------------------------------------------
 *
 * TclIcuCleanup --
 *
 *	Called whenever a command referencing the ICU function table is
 *	deleted. When the reference count drops to zero, the table is released
 *	and the ICU shared libraries are unloaded.
 *
 *------------------------------------------------------------------------
 */
static void
TclIcuCleanup(
    TCL_UNUSED(void *))
{
    Tcl_MutexLock(&icu_mutex);
    if (icu_fns.nopen-- <= 1) {
	int i;
	if (u_cleanup != NULL) {
	    u_cleanup();
	}
	for (i = 0; i < (int)(sizeof(icu_fns.libs) / sizeof(icu_fns.libs[0]));
		++i) {
	    if (icu_fns.libs[i] != NULL) {
		Tcl_FSUnloadFile(NULL, icu_fns.libs[i]);
	    }
	}
	memset(&icu_fns, 0, sizeof(icu_fns));
    }
    Tcl_MutexUnlock(&icu_mutex);
}

/*
 *------------------------------------------------------------------------
 *
 * IcuFindSymbol --
 *
 *	Finds an ICU symbol in a shared library and returns its value.
 *
 *	Caller must be holding icu_mutex lock.
 *
 * Results:
 *	Returns the symbol value or NULL if not found.
 *
 *------------------------------------------------------------------------
 */
static void *
IcuFindSymbol(
    Tcl_LoadHandle loadH,	/* Handle to shared library containing symbol */
    const char *name,		/* Name of function */
    const char *suffix)		/* Suffix that may be present */
{
    /*
     * ICU symbols may have a version suffix depending on how it was built.
     * Rather than try both forms every time, suffixConvention remembers if a
     * suffix is needed (all functions will have it, or none will)
     * 0 - don't know, 1 - have suffix, -1 - no suffix
     */
    static int suffixConvention = 0;
    char symbol[256];
    void *value = NULL;

    /* Note we only update suffixConvention on a positive result */

    strcpy(symbol, name);
    if (suffixConvention <= 0) {
	/* Either don't need suffix or don't know if we do */
	value = Tcl_FindSymbol(NULL, loadH, symbol);
	if (value) {
	    suffixConvention = -1; /* Remember that no suffixes present */
	    return value;
	}
    }
    if (suffixConvention >= 0) {
	/* Either need suffix or don't know if we do */
	strcat(symbol, suffix);
	value = Tcl_FindSymbol(NULL, loadH, symbol);
	if (value) {
	    suffixConvention = 1;
	}
    }
    return value;
}

/*
 *------------------------------------------------------------------------
 *
 * TclIcuInit --
 *
 *	Load the ICU commands into the given interpreter. If the ICU
 *	commands have never previously been loaded, the ICU libraries are
 *	loaded first.
 *
 *------------------------------------------------------------------------
 */
static void
TclIcuInit(
    Tcl_Interp *interp)
{
    Tcl_MutexLock(&icu_mutex);
    char icuversion[4] = "_80"; /* Highest ICU version + 1 */

    /*
     * The initialization below clones the one from Tk. May need revisiting.
     * ICU shared library names as well as function names *may* be versioned.
     * See https://unicode-org.github.io/icu/userguide/icu4c/packaging.html
     * for the gory details.
     */
    if (icu_fns.nopen == 0) {
	int i = 0;
	Tcl_Obj *nameobj;
	static const char *const iculibs[] = {
#if defined(_WIN32)
#  define DLLNAME "icu%s%s.dll"
	    "icuuc??.dll", /* Windows, user-provided */
	    NULL,
	    "cygicuuc??.dll", /* When running under Cygwin */
#elif defined(__CYGWIN__)
#  define DLLNAME "cygicu%s%s.dll"
	    "cygicuuc??.dll",
#elif defined(MAC_OSX_TCL)
#  define DLLNAME "libicu%s.%s.dylib"
	    "libicuuc.??.dylib",
#else
#  define DLLNAME "libicu%s.so.%s"
	    "libicuuc.so.??",
#endif
	    NULL
	};

	/* Going back down to ICU version 60 */
	while ((icu_fns.libs[0] == NULL) && (icuversion[1] >= '6')) {
	    if (--icuversion[2] < '0') {
		icuversion[1]--;
		icuversion[2] = '9';
	    }
#if defined(__CYGWIN__)
	    i = 2;
#else
	    i = 0;
#endif
	    while (iculibs[i] != NULL) {
		Tcl_ResetResult(interp);
		nameobj = Tcl_NewStringObj(iculibs[i], TCL_AUTO_LENGTH);
		char *nameStr = Tcl_GetString(nameobj);
		char *p = strchr(nameStr, '?');

		if (p != NULL) {
		    memcpy(p, icuversion+1, 2);
		}
		Tcl_IncrRefCount(nameobj);
		if (Tcl_LoadFile(interp, nameobj, NULL, 0, NULL,
			&icu_fns.libs[0]) == TCL_OK) {
		    if (p == NULL) {
			icuversion[0] = '\0';
		    }
		    Tcl_DecrRefCount(nameobj);
		    break;
		}
		Tcl_DecrRefCount(nameobj);
		++i;
	    }
	}
	if (icu_fns.libs[0] != NULL) {
	    /* Loaded icuuc, load others with the same version */
	    nameobj = Tcl_ObjPrintf(DLLNAME, "i18n", icuversion+1);
	    Tcl_IncrRefCount(nameobj);
	    /* Ignore errors. Calls to contained functions will fail. */
	    (void) Tcl_LoadFile(interp, nameobj, NULL, 0, NULL, &icu_fns.libs[1]);
	    Tcl_DecrRefCount(nameobj);
	}
#ifdef _WIN32
	/*
	 * On Windows, if no ICU install found, look for the system's
	 * (Win10 1703 or later). There are two cases. Newer systems
	 * have icu.dll containing all functions. Older systems have
	 * icucc.dll and icuin.dll
	 */
	if (icu_fns.libs[0] == NULL) {
	    Tcl_ResetResult(interp);
	    nameobj = Tcl_NewStringObj("icu.dll", TCL_AUTO_LENGTH);
	    Tcl_IncrRefCount(nameobj);
	    if (Tcl_LoadFile(interp, nameobj, NULL, 0, NULL, &icu_fns.libs[0])
		    == TCL_OK) {
		/* Reload same for second set of functions. */
		(void) Tcl_LoadFile(interp, nameobj, NULL, 0, NULL,
			&icu_fns.libs[1]);
		/* Functions do NOT have version suffixes */
		icuversion[0] = '\0';
	    }
	    Tcl_DecrRefCount(nameobj);
	}
	if (icu_fns.libs[0] == NULL) {
	    /* No icu.dll. Try last fallback */
	    Tcl_ResetResult(interp);
	    nameobj = Tcl_NewStringObj("icuuc.dll", TCL_AUTO_LENGTH);
	    Tcl_IncrRefCount(nameobj);
	    if (Tcl_LoadFile(interp, nameobj, NULL, 0, NULL, &icu_fns.libs[0])
		    == TCL_OK) {
		Tcl_DecrRefCount(nameobj);
		nameobj = Tcl_NewStringObj("icuin.dll", TCL_AUTO_LENGTH);
		Tcl_IncrRefCount(nameobj);
		(void) Tcl_LoadFile(interp, nameobj, NULL, 0, NULL,
			&icu_fns.libs[1]);
		/* Functions do NOT have version suffixes */
		icuversion[0] = '\0';
	    }
	    Tcl_DecrRefCount(nameobj);
	}
#endif // _WIN32

	/* Symbol may have version (Linux), or not (Windows, FreeBSD) */

#define ICUUC_SYM(name) \
    do {								\
	icu_fns._##name = (fn_##name)					\
		IcuFindSymbol(icu_fns.libs[0], #name, icuversion);	\
    } while (0)

	if (icu_fns.libs[0] != NULL) {
	    ICUUC_SYM(u_cleanup);
	    ICUUC_SYM(u_errorName);
	    ICUUC_SYM(u_strFromUTF32);
	    ICUUC_SYM(u_strFromUTF32WithSub);
	    ICUUC_SYM(u_strToUTF32);
	    ICUUC_SYM(u_strToUTF32WithSub);

	    ICUUC_SYM(ucnv_close);
	    ICUUC_SYM(ucnv_countAliases);
	    ICUUC_SYM(ucnv_countAvailable);
	    ICUUC_SYM(ucnv_fromUChars);
	    ICUUC_SYM(ucnv_getAlias);
	    ICUUC_SYM(ucnv_getAvailableName);
	    ICUUC_SYM(ucnv_open);
	    ICUUC_SYM(ucnv_setFromUCallBack);
	    ICUUC_SYM(ucnv_setToUCallBack);
	    ICUUC_SYM(ucnv_toUChars);
	    ICUUC_SYM(UCNV_FROM_U_CALLBACK_STOP);
	    ICUUC_SYM(UCNV_TO_U_CALLBACK_STOP);

	    ICUUC_SYM(ubrk_open);
	    ICUUC_SYM(ubrk_close);
	    ICUUC_SYM(ubrk_preceding);
	    ICUUC_SYM(ubrk_following);
	    ICUUC_SYM(ubrk_previous);
	    ICUUC_SYM(ubrk_next);
	    ICUUC_SYM(ubrk_setText);

	    ICUUC_SYM(uenum_close);
	    ICUUC_SYM(uenum_count);
	    ICUUC_SYM(uenum_next);

	    ICUUC_SYM(unorm2_getNFCInstance);
	    ICUUC_SYM(unorm2_getNFDInstance);
	    ICUUC_SYM(unorm2_getNFKCInstance);
	    ICUUC_SYM(unorm2_getNFKDInstance);
	    ICUUC_SYM(unorm2_normalize);
#undef ICUUC_SYM
	}

#define ICUIN_SYM(name) \
    do {								\
	icu_fns._##name = (fn_##name)					\
		IcuFindSymbol(icu_fns.libs[1], #name, icuversion);	\
    } while (0)

	if (icu_fns.libs[1] != NULL) {
	    ICUIN_SYM(ucsdet_close);
	    ICUIN_SYM(ucsdet_detect);
	    ICUIN_SYM(ucsdet_detectAll);
	    ICUIN_SYM(ucsdet_getName);
	    ICUIN_SYM(ucsdet_getAllDetectableCharsets);
	    ICUIN_SYM(ucsdet_open);
	    ICUIN_SYM(ucsdet_setText);
#undef ICUIN_SYM
	}
    }

    if (icu_fns.libs[0] != NULL) {
	/*
	 * Note refcounts updated BEFORE command definition to protect
	 * against self redefinition.
	 */
	if (icu_fns.libs[1] != NULL) {
	    /* Commands needing both libraries */

	    /* Ref count number of commands */
	    icu_fns.nopen += 3;
	    Tcl_CreateObjCommand2(interp,  "::tcl::unsupported::icu::convertto",
		    IcuConverttoObjCmd, 0, TclIcuCleanup);
	    Tcl_CreateObjCommand2(interp,  "::tcl::unsupported::icu::convertfrom",
		    IcuConvertfromObjCmd, 0, TclIcuCleanup);
	    Tcl_CreateObjCommand2(interp,  "::tcl::unsupported::icu::detect",
		    IcuDetectObjCmd, 0, TclIcuCleanup);
	}

	/* Commands needing only libs[0] (icuuc) */

	/* Ref count number of commands */
	icu_fns.nopen += 3; /* UPDATE AS CMDS ADDED/DELETED BELOW */
	Tcl_CreateObjCommand2(interp, "::tcl::unsupported::icu::converters",
		IcuConverterNamesObjCmd, 0, TclIcuCleanup);
	Tcl_CreateObjCommand2(interp, "::tcl::unsupported::icu::aliases",
		IcuConverterAliasesObjCmd, 0, TclIcuCleanup);
	Tcl_CreateObjCommand2(interp, "::tcl::unsupported::icu::normalize",
		IcuNormalizeObjCmd, 0, TclIcuCleanup);
    }

    Tcl_MutexUnlock(&icu_mutex);
}

/*
 *------------------------------------------------------------------------
 *
 * TclLoadIcuObjCmd --
 *
 *	Loads and initializes ICU
 *
 * Results:
 *	TCL_OK / TCL_ERROR.
 *
 * Side effects:
 *	Interpreter result holds result or error message.
 *
 *------------------------------------------------------------------------
 */
int
TclLoadIcuObjCmd(
    TCL_UNUSED(void *),
    Tcl_Interp *interp,		/* Current interpreter. */
    Tcl_Size objc,		/* Number of arguments. */
    Tcl_Obj *const *objv)	/* Argument objects. */
{
    if (objc != 1) {
	Tcl_WrongNumArgs(interp, 1 , objv, "");
	return TCL_ERROR;
    }
    TclIcuInit(interp);
    return TCL_OK;
}

/*
 * Local Variables:
 * mode: c
 * c-basic-offset: 4
 * fill-column: 78
 * coding: utf-8
 * End:
 */
