/*
 * Copyright 2020 University Corporation for Atmospheric Research
 *
 * This file is part of the UDUNITS-2 package.  See the file COPYRIGHT
 * in the top-level source-directory of the package for copying and
 * redistribution conditions.
 */
/*
 * Unit-to-identifier map.
 */

/*LINTLIBRARY*/

#include "config.h"

#include "udunits2.h"
#include "unitAndId.h"
#include "unitToIdMap.h"		/* this module's API */
#include "systemMap.h"

#include <assert.h>
#include <errno.h>

#ifdef _MSC_VER
#include "tsearch.h"
#else
#include <search.h>
#endif

#include <stdlib.h>

#include <string.h>

typedef struct {
    void*		ascii;
    void*		latin1;
    void*		utf8;
} UnitToIdMap;

static SystemMap*	systemToUnitToName = NULL;
static SystemMap*	systemToUnitToSymbol = NULL;


/******************************************************************************
 * Miscellaneous Functions:
 ******************************************************************************/

/*
 * Unconditionally converts an ISO Latin-1 string into a UTF-8 string.
 *
 * Arguments:
 *	latin1String	Pointer to the ISO Latin-1 string.
 * Returns:
 *	NULL		Failure.  See errno.
 *	else		Pointer to the equivalent UTF-8 string.  Should be freed
 *			when no longer needed.
 */
static char*
latin1ToUtf8(
    const char* const	latin1String)
{
    int			nchar;
    const char*		inp;
    char*		outp;
    char*		utf8String;

    for (nchar = 0, inp = latin1String; *inp; ++inp, ++nchar)
	if ((*inp & 0x80U) != 0)
	    nchar++;

    utf8String = malloc(nchar+1);

    if (utf8String != NULL) {
	for (inp = latin1String, outp = utf8String; *inp; ++inp, ++outp) {
	    if ((*inp & 0x80U) == 0) {
		*outp = *inp;
	    }
	    else {
		*outp++ = (char)(0xC0U | ((unsigned)*inp >> 6));
		*outp = (char)(0x80U | (*inp & 0x3FU));
	    }
	}

	*outp = 0;
    }

    return utf8String;
}

/*
 * Adjust a given encoding according to a string.  Because ASCII is a subset of
 * ISO Latin-1 and because a UTF-8 encoded string must follow certain rules,
 * it's possible for strings to be mis-encoded and for an encoding specification
 * to be too restrictive or over-generous.  If the encoding specification is
 * ASCII and the given string contains a character with the high-order bit
 * set, then the encoding will be changed to ISO Latin-1.  If the encoding
 * specification is ISO Latin-1 but the string doesn't contain a character with
 * the high-order bit set, then the encoding will be changed to ASCII.  If the
 * encoding specification is UTF-8 but the string doesn't follow the UTF-8
 * encoding rules, then the function will error-return.
 *
 * Arguments:
 *	encoding	Pointer to the presumptive encoding.  Might be modified
 *			on return to reflect the actual, most restrictive,
 *			encoding of "string".
 *	string		The string to be checked.
 * Returns:
 *	0		Success.  "*encoding" might be modified.
 *	-1		Failure.  "string" doesn't conform to "encoding".
 */
static int
adjustEncoding(
    ut_encoding* const	encoding,
    const char* 	string)
{
    int		status = 0;		/* success */

    if (*encoding == UT_ASCII) {
	for (; *string && ((*string & 0x80U) == 0); string++)
	    ;

	if (*string != 0)
	    *encoding = UT_LATIN1;
    }
    else if (*encoding == UT_LATIN1) {
	for (; *string && ((*string & 0x80U) == 0); string++)
	    ;

	if (*string == 0)
	    *encoding = UT_ASCII;
    }
    else if (*encoding == UT_UTF8) {
	for (; *string; string++) {
	    if (*string & 0x80U) {
		if ((*string & 0xE0U) == 0xC0U) {
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		}
		else if ((*string & 0xF0U) == 0xE0U) {
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		}
		else if ((*string & 0xF8U) == 0xF0U) {
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		    if ((*++string & 0xC0U) != 0x80U)
			break;
		}
	    }
	}

	if (*string)
	    status = -1;
    }

    return status;
}


/******************************************************************************
 * Internal Map Functions:
 ******************************************************************************/

static int
compareUnits(
    const void* const	entry1,
    const void* const	entry2)
{
    return ut_compare(((const UnitAndId*)entry1)->unit,
	((const UnitAndId*)entry2)->unit);
}


/*
 * Selects a unit-and-identifier tree corresponding to a given encoding.
 *
 * Arguments:
 *	map		The unit-to-id map.
 *	encoding	The encoding.
 * Returns:
 *	Pointer to the root of the unit-and-identifier tree in "map" that
 *	corresponds to "encoding".
 */
static void**
selectTree(
    UnitToIdMap* const	unitToIdMap,
    const ut_encoding	encoding)
{
    return
	encoding == UT_ASCII
	    ? &unitToIdMap->ascii
	    : encoding == UT_LATIN1
		? &unitToIdMap->latin1
		: &unitToIdMap->utf8;
}


/*
 * Returns a new instance of a unit-to-identifier map.
 *
 * Returns:
 *	NULL	Failure.  See "errno".
 *	else	Pointer to a new unit-to-identifier map.
 */
static UnitToIdMap*
utimNew(void)
{
    UnitToIdMap* const	map = malloc(sizeof(UnitToIdMap));

    if (map != NULL) {
	map->ascii = NULL;
	map->latin1 = NULL;
	map->utf8 = NULL;
    }

    return map;
}


/*
 * Frees a unit-to-identifier map.  All entries in all encodings are freed.
 *
 * Arguments:
 *	map		Pointer to the map to be freed.
 */
static void
utimFree(
    UnitToIdMap*	map)
{
    if (map != NULL) {
	ut_encoding	encodings[] = {UT_ASCII, UT_LATIN1, UT_UTF8};
	int		i;

	for (i = 0; i < sizeof(encodings)/sizeof(encodings[0]); ++i) {
	    void**	rootp = selectTree(map, encodings[i]);

	    while (*rootp != NULL) {
		UnitAndId*	uai = **(UnitAndId***)rootp;

		(void)tdelete(uai, rootp, compareUnits);
		uaiFree(uai);
	    }
	}

	free(map);
    }
}


/*
 * Adds an entry to a unit-to-identifier map.
 *
 * Arguments:
 *	map		Pointer to unit-to-identifier map.
 *	unit		The unit.  May be freed upon return.
 *	id		The identifier.  May be freed upon return.
 *	encoding	The ostensible encoding of "id".
 * Returns:
 *	UT_BAD_ARG	"id" is inconsistent with "encoding".
 *	UT_OS		Operating-system error.  See "errno".
 *	UT_EXISTS	"unit" already maps to a different identifier.
 *	UT_SUCCESS	Success.
 */
static ut_status
utimAdd(
    UnitToIdMap* const	map,
    const ut_unit*	unit,
    const char* const	id,
    ut_encoding		encoding)
{
    ut_status		status;

    assert(map != NULL);
    assert(unit != NULL);
    assert(id != NULL);

    if (adjustEncoding(&encoding, id)) {
	status = UT_BAD_ARG;
        ut_set_status(status);
	ut_handle_error_message("Identifier not in given encoding");
    }
    else {
	UnitAndId*	targetEntry = uaiNew(unit, id);

	if (targetEntry == NULL) {
            status = ut_get_status();
        }
        else {
	    void**	rootp = selectTree(map, encoding);

	    UnitAndId**	treeEntry = tsearch(targetEntry, rootp, compareUnits);

	    if (treeEntry == NULL) {
		status = UT_OS;
                ut_set_status(status);
		ut_handle_error_message(strerror(errno));
		ut_handle_error_message("Couldn't add search-tree entry");
		uaiFree(targetEntry);
	    }
	    else {
		if (strcmp((*treeEntry)->id, id) != 0) {
		    status = UT_EXISTS;
                    ut_set_status(status);
		    ut_handle_error_message("Unit already maps to \"%s\"",
			(*treeEntry)->id);
		}
		else {
		    status = UT_SUCCESS;
		}

                if (targetEntry != *treeEntry)
                    uaiFree(targetEntry);
	    }
	}				/* "targetEntry" allocated */
    }					/* valid arguments */

    return status;
}


/*
 * Removes an entry from a unit-to-identifier map.
 *
 * Arguments:
 *	map		Pointer to the unit-to-identifier map.
 *	unit		The unit.  May be freed upon return.
 *	encoding	The encoding to be removed.
 * Returns:
 *	UT_SUCCESS	Success.
 */
static ut_status
utimRemove(
    UnitToIdMap* const	map,
    const ut_unit*	unit,
    ut_encoding		encoding)
{
    UnitAndId		targetEntry;
    UnitAndId**		treeEntry;

    assert(map != NULL);
    assert(unit != NULL);

    targetEntry.unit = (ut_unit*)unit;
    treeEntry = tfind(&targetEntry, selectTree(map, encoding), compareUnits);

    if (treeEntry != NULL && *treeEntry != NULL) {
	UnitAndId*	uai = *treeEntry;

	(void)tdelete(uai, selectTree(map, encoding), compareUnits);
	uaiFree(uai);
    }

    return UT_SUCCESS;
}


/*
 * Returns the unit-and-identifier whose ASCII identifier corresponding to a
 * unit.
 *
 * Arguments:
 *	map	The unit-to-identifier map.
 *	unit	The unit to be used as the key in the search.
 * Returns:
 *	NULL	The map doesn't contain an entry corresponding to "unit" whose
 *		identifier is in ASCII.
 *	else	Pointer to the entry corresponding to "unit" whose identifier is
 *		in ASCII.
 */
static UnitAndId*
utimFindAsciiByUnit(
    UnitToIdMap* const	map,
    const ut_unit* const	unit)
{
    UnitAndId	targetEntry;
    UnitAndId**	treeEntry;

    targetEntry.unit = (ut_unit*)unit;
    treeEntry = tfind(&targetEntry, &map->ascii, compareUnits);

    return treeEntry == NULL ? NULL : *treeEntry;
}


/*
 * Finds a unit-search-entry with a Latin-1 identifier correcponding to a unit.
 *
 * Arguments:
 *	map	The unit-to-identifier map.
 *	unit	The unit to be used as the key in the search.
 * Returns:
 *	NULL	The map doesn't contain an entry corresponding to "unit" whose
 *		identifier is in Latin-1.
 *	else	Pointer to the entry corresponding to "unit" whose identifier is
 *		in Latin-1 (and might, actually, be in ASCII).
 */
static UnitAndId*
utimFindLatin1ByUnit(
    UnitToIdMap* const	map,
    const ut_unit* const	unit)
{
    UnitAndId	targetEntry;
    UnitAndId**	treeEntry;

    targetEntry.unit = (ut_unit*)unit;
    treeEntry = tfind(&targetEntry, &map->latin1, compareUnits);

    if (treeEntry == NULL)
	treeEntry = tfind(&targetEntry, &map->ascii, compareUnits);

    return treeEntry == NULL ? NULL : *treeEntry;
}


/*
 * Finds an entry with a UTF-8 identifier corresponding to a unit.
 *
 * Arguments:
 *	map	The unit-to-identifier map.
 *	unit	The unit to be used as the key in the search.
 * Returns:
 *	NULL	The map doesn't contain an entry corresponding to "unit" whose
 *		identifier is in UTF-8.
 *	else	Pointer to the entry corresponding to "unit" whose identifier is
 *		in UTF-8 (and might, actually, be in ASCII).
 */
static UnitAndId*
utimFindUtf8ByUnit(
    UnitToIdMap* const	        map,
    const ut_unit* const	unit)
{
    UnitAndId	targetEntry;
    UnitAndId**	treeEntry = NULL;	/* failure */

    targetEntry.unit = (ut_unit*)unit;
    treeEntry = tfind(&targetEntry, &map->utf8, compareUnits);

    if (treeEntry == NULL) {
	treeEntry = tfind(&targetEntry, &map->latin1, compareUnits);

	if (treeEntry == NULL) {
	    treeEntry = tfind(&targetEntry, &map->ascii, compareUnits);
	}
	else {
	    /*
	     * Create the UTF-8 version of the Latin-1 identifier and add it to
	     * the UTF-8 unit-to-id map so that it will be found next time.
	     */
	    char* const	id = latin1ToUtf8((*treeEntry)->id);

	    if (id == NULL) {
		ut_set_status(UT_OS);
		ut_handle_error_message(strerror(errno));
		ut_handle_error_message(
		    "Couldn't convert identifier from ISO-8859-1 to UTF-8");
		treeEntry = NULL;
	    }
	    else {
		UnitAndId*	newEntry = uaiNew(unit, id);

		if (newEntry != NULL) {
		    treeEntry = tsearch(newEntry, &map->utf8, compareUnits);

		    if (treeEntry == NULL) {
			uaiFree(newEntry);
			ut_set_status(UT_OS);
			ut_handle_error_message(strerror(errno));
			ut_handle_error_message(
                            "Couldn't add unit-and-identifier to search-tree");
		    }
		}

		free(id);
	    }				/* UTF-8 identifier created */
	}				/* found Latin-1 identifier */
    }					/* no UTF-8 identifier */

    return treeEntry == NULL ? NULL : *treeEntry;
}


/*
 * Adds an entry to the unit-to-identifier map associated with a unit-system.
 *
 * Arguments:
 *	sytemMap	Address of the pointer to the
 *			system-to-unit-to-identifier map.
 *	unit		The unit.  May be freed upon return.
 *	id		The identifier.  May be freed upon return.
 *	encoding	The ostensible encoding of "id".
 * Returns:
 *	UT_BAD_ARG	"unit" or "id" is NULL, or "id" is inconsistent with
 *                      "encoding".
 *	UT_OS		Operating-system error.  See "errno".
 *	UT_EXISTS	"unit" already maps to a different identifier.
 *	UT_SUCCESS	Success.
 */
static ut_status
mapUnitToId(
    SystemMap** const		systemMap,
    const ut_unit* const	unit,
    const char* const		id,
    ut_encoding			encoding)
{
    ut_status		status;

    assert(systemMap != NULL);

    if (unit == NULL || id == NULL) {
	status = UT_BAD_ARG;
    }
    else {
	if (*systemMap == NULL) {
	    *systemMap = smNew();

	    if (*systemMap == NULL)
		status = UT_OS;
	}

	if (*systemMap != NULL) {
	    UnitToIdMap** const	unitToIdMap =
		(UnitToIdMap**)smSearch(*systemMap, ut_get_system(unit));

	    if (unitToIdMap == NULL) {
		status = UT_OS;
	    }
	    else {
		if (*unitToIdMap == NULL) {
		    *unitToIdMap = utimNew();

		    if (*unitToIdMap == NULL)
			status = UT_OS;
		}

		if (*unitToIdMap != NULL)
		    status = utimAdd(*unitToIdMap, unit, id, encoding);
	    }
	}
    }

    return status;
}


/*
 * Removes an entry from the unit-to-identifier map associated with a
 * unit-system.
 *
 * Arguments:
 *	sytemMap	Pointer to the system-to-unit-to-identifier map.
 *	unit		The unit.  May be freed upon return.
 *	encoding	The ostensible encoding of "id".
 * Returns:
 *	UT_BAD_ARG	"systemMap" is NULL.
 *	UT_BAD_ARG	"unit" is NULL.
 *	UT_SUCCESS	Success.
 */
static ut_status
unmapUnitToId(
    SystemMap* const		systemMap,
    const ut_unit* const	unit,
    ut_encoding			encoding)
{
    ut_status		status;

    if (systemMap == NULL || unit == NULL) {
	status = UT_BAD_ARG;
    }
    else {
	UnitToIdMap** const	unitToIdMap =
	    (UnitToIdMap**)smFind(systemMap, ut_get_system(unit));

	status =
	    (unitToIdMap == NULL || *unitToIdMap == NULL)
		? UT_SUCCESS
		: utimRemove(*unitToIdMap, unit, encoding);
    }

    return status;
}


/*
 * Returns the identifier in a given encoding to which a unit associated with
 * a unit-system maps.
 *
 * Arguments:
 *	systemMap	Pointer to the system-to-unit-to-id map.
 *	unit		Pointer to the unit whose identifier should be returned.
 *	encoding	The desired encoding of the identifier.
 * Returns:
 *	NULL		Failure.  "ut_get_status()" will be
 *			    UT_BAD_ARG	"unit" was NULL.
 *	else		Pointer to the identifier in the given encoding
 *			associated with "unit".
 */
static const char*
getId(
    SystemMap* const	systemMap,
    const ut_unit* const	unit,
    const ut_encoding	encoding)
{
    const char*	id = NULL;		/* failure */

    if (unit == NULL) {
	ut_set_status(UT_BAD_ARG);
	ut_handle_error_message("NULL unit argument");
    }
    else {
	UnitToIdMap** const	unitToId =
	    (UnitToIdMap**)smFind(systemMap, ut_get_system(unit));

	if (unitToId != NULL) {
	    UnitAndId*	mapEntry =
		encoding == UT_LATIN1
		    ? utimFindLatin1ByUnit(*unitToId, unit)
		    : encoding == UT_UTF8
			? utimFindUtf8ByUnit(*unitToId, unit)
			: utimFindAsciiByUnit(*unitToId, unit);

	    if (mapEntry != NULL)
		id = mapEntry->id;
	}
    }

    return id;
}


/******************************************************************************
 * Public API:
 ******************************************************************************/


/*
 * Adds a mapping from a unit to a name.
 *
 * Arguments:
 *	unit		Pointer to the unit to be mapped to "name".  May be
 *			freed upon return.
 *	name		Pointer to the name to be mapped-to by "unit".  May be
 *			freed upon return.
 *	encoding	The encoding of "name".
 * Returns:
 *	UT_SUCCESS	Success.
 *	UT_BAD_ARG	"unit" or "name" is NULL, or "name" is not in the
 *                      specified encoding.
 *	UT_OS		Operating-system error.  See "errno".
 *	UT_EXISTS	"unit" already maps to a name.
 */
ut_status
ut_map_unit_to_name(
    const ut_unit* const	unit,
    const char* const		name,
    ut_encoding			encoding)
{
    ut_set_status(mapUnitToId(&systemToUnitToName, unit, name, encoding));

    return ut_get_status();
}


/*
 * Removes a mapping from a unit to a name.
 *
 * Arguments:
 *	unit		Pointer to the unit.  May be freed upon return.
 *	encoding	The encoding to be removed.  No other encodings will be
 *			removed.
 * Returns:
 *	UT_BAD_ARG	"unit" is NULL.
 *	UT_SUCCESS	Success.
 */
ut_status
ut_unmap_unit_to_name(
    const ut_unit* const	unit,
    ut_encoding			encoding)
{
    ut_set_status(unmapUnitToId(systemToUnitToName, unit, encoding));

    return ut_get_status();
}


/*
 * Adds a mapping from a unit to a symbol.
 *
 * Arguments:
 *	unit		Pointer to the unit to be mapped to "symbol".  May be
 *			freed upon return.
 *	symbol		Pointer to the symbol to be mapped-to by "unit".  May
 *			be freed upon return.
 *	encoding	The encoding of "symbol".
 * Returns:
 *	UT_SUCCESS	Success.
 *	UT_BAD_ARG	"unit" or "symbol" is NULL.
 *	UT_OS		Operating-system error.  See "errno".
 *	UT_EXISTS	"unit" already maps to a symbol.
 */
ut_status
ut_map_unit_to_symbol(
    const ut_unit*		unit,
    const char* const		symbol,
    ut_encoding			encoding)
{
    ut_set_status(mapUnitToId(&systemToUnitToSymbol, unit, symbol, encoding));

    return ut_get_status();
}


/*
 * Removes a mapping from a unit to a symbol.
 *
 * Arguments:
 *	unit		Pointer to the unit to be unmapped to a symbol.  May be
 *			freed upon return.
 *	encoding	The encoding to be removed.  The mappings for "unit" in
 *			other encodings will not be removed.
 * Returns:
 *	UT_SUCCESS	Success.
 *	UT_BAD_ARG	"unit" is NULL.
 */
ut_status
ut_unmap_unit_to_symbol(
    const ut_unit* const	unit,
    ut_encoding			encoding)
{
    ut_set_status(unmapUnitToId(systemToUnitToSymbol, unit, encoding));

    return ut_get_status();
}


/*
 * Returns the name in a given encoding to which a unit maps.
 *
 * Arguments:
 *	unit		Pointer to the unit whose name should be returned.
 *	encoding	The desired encoding of the name.
 * Returns:
 *	NULL		Failure.  "ut_get_status()" will be
 *			    UT_BAD_ARG		"unit" is NULL.
 *			    UT_SUCCESS		"unit" doesn't map to a name in
 *						in the given encoding.
 *	else		Pointer to the name in the given encoding to which
 *			"unit" maps.
 */
const char*
ut_get_name(
    const ut_unit* const	unit,
    const ut_encoding	encoding)
{
    ut_set_status(UT_SUCCESS);

    return getId(systemToUnitToName, unit, encoding);
}


/*
 * Returns the symbol in a given encoding to which a unit maps.
 *
 * Arguments:
 *	unit		Pointer to the unit whose symbol should be returned.
 *	encoding	The desired encoding of the symbol.
 * Returns:
 *	NULL		Failure.  "ut_get_status()" will be
 *			    UT_BAD_ARG		"unit" is NULL.
 *			    UT_SUCCESS		"unit" doesn't map to a symbol
 *						in the given encoding.
 *	else		Pointer to the symbol in the given encoding to which
 *			"unit" maps.
 */
const char*
ut_get_symbol(
    const ut_unit* const	unit,
    const ut_encoding	encoding)
{
    ut_set_status(UT_SUCCESS);

    return getId(systemToUnitToSymbol, unit, encoding);
}


/*
 * Frees resources associated with a unit-system.
 *
 * Arguments:
 *	system		Pointer to the unit-system to have its associated
 *			resources freed.
 */
void
utimFreeSystem(
    ut_system*	system)
{
    if (system != NULL) {
	SystemMap*	systemMaps[2];
	int		i;

	systemMaps[0] = systemToUnitToName;
	systemMaps[1] = systemToUnitToSymbol;

	for (i = 0; i < 2; i++) {
	    if (systemMaps[i] != NULL) {
		UnitToIdMap** const	unitToId =
		    (UnitToIdMap**)smFind(systemMaps[i], system);

		if (unitToId != NULL)
		    utimFree(*unitToId);

		smRemove(systemMaps[i], system);
	    }
	}
    }
}
