/*
 * Copyright 2013 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.
 */

#include <errno.h>
#ifdef _MSC_VER
#include "tsearch.h"
#else
#include <search.h>
#endif
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "udunits.h"

static ut_system*	unitSystem = NULL;
static ut_unit*		encodedTimeUnit = NULL;
static ut_unit*		second = NULL;
static char*		buffer;
static int		buflen = 80;
static void*		unit2s = NULL;

/*
 * Initialize the units(3) package.
 */
int
utInit(
    const char	*path)
{
    int status;

    (void)ut_set_error_message_handler(ut_ignore);
    if (unitSystem != NULL) {
	ut_free_system(unitSystem);
	unitSystem = NULL;
    }
    unitSystem = ut_read_xml(NULL);
    if (unitSystem == NULL) {
	status = ut_get_status() == UT_PARSE
	    ? UT_ESYNTAX
	    : UT_EIO;
    }
    else {
	second = ut_get_unit_by_name(unitSystem, "second");
	encodedTimeUnit =
	    ut_offset_by_time(second, ut_encode_time(2001, 1, 1, 0, 0, 0.0));
	buffer = malloc(buflen);
	if (buffer == NULL) {
	    buflen = 0;
	    status = UT_EALLOC;
	}
	else {
	    status = 0;
	}
    }
    return status;
}

/*
 * Indicate if the units(3) package has been initialized.
 */
int
utIsInit()
{
    return unitSystem != NULL;
}

static int
compare(
    const void*	key1,
    const void*	key2)
{
    return key1 < key2 ? -1 : key1 == key2 ? 0 : 1;
}

static void
freeIfAllocated(
    utUnit* const	unit)
{
    if (tdelete(unit->unit2, &unit2s, compare) != NULL) {
	ut_free(unit->unit2);
    }
    unit->unit2 = NULL;
}

void
utFree(
    utUnit* const	unit)
{
    freeIfAllocated(unit);
}

void
utIni(
    utUnit* const	unit)
{
    if (unit != NULL) {
	unit->unit2 = NULL;
    }
}

static int
setUnit(
    utUnit* const	unit,
    ut_unit* const	unit2)
{
    int	status;

    if (tsearch(unit2, &unit2s, compare) == NULL) {
	status = UT_EALLOC;
    }
    else {
	freeIfAllocated(unit);
	unit->unit2 = unit2;
	status = 0;
    }
    return status;
}

/*
 * Decode a formatted unit specification into a unit-structure.
 */
int
utScan(
    const char	*spec,
    utUnit	*unit)
{
    int		status;

    if (spec == NULL || unit == NULL) {
	status = UT_EINVALID;
    }
    else {
	ut_unit*	ut_unit = ut_parse(unitSystem, spec, UT_ASCII);

	if (ut_unit != NULL) {
	    status = setUnit(unit, ut_unit);
	}
	else {
	    status = ut_get_status();
	    if (status == UT_BAD_ARG) {
		status = unitSystem == NULL
		    ? UT_ENOINIT
		    : UT_EINVALID;
	    }
	    else if (status == UT_SYNTAX) {
		status =  UT_ESYNTAX;
	    }
	    else if (status == UT_UNKNOWN) {
		status =  UT_EUNKNOWN;
	    }
	    else {
		status = UT_EALLOC;
	    }
	}
    }
    return status;
}

/*
 * Convert a temporal value into a UTC Gregorian date and time.
 */
int
utCalendar(
    double		value,
    const utUnit	*unit,
    int			*year,
    int			*month,
    int			*day,
    int			*hour,
    int			*minute,
    float		*second)
{
    int		status = 0;	/* success */

    cv_converter* converter = ut_get_converter(unit->unit2, encodedTimeUnit);
    if (converter == NULL) {
	status = encodedTimeUnit == NULL ? UT_ENOINIT : UT_EINVALID;
    }
    else {
	double	encodedTime = cv_convert_double(converter, value);
	double	sec, res;

	ut_decode_time(encodedTime, year, month, day, hour, minute, &sec, &res);
	*second = (float)sec;
	cv_free(converter);
    }
    return status;
}

/*
 *	Convert a date into a temporal value.  The date is assumed to
 *	use the Gregorian calendar if on or after noon, October 15, 1582;
 *	otherwise, the date is assumed to use the Julian calendar.
 */
int
utInvCalendar(
    int			year,
    int			month,
    int			day,
    int			hour,
    int			minute,
    double		second,
    const utUnit	*unit,
    double		*value)
{
    int		status = 0;	/* success */

    cv_converter* converter = ut_get_converter(encodedTimeUnit, unit->unit2);
    if (converter == NULL) {
	status = encodedTimeUnit == NULL ? UT_ENOINIT : UT_EINVALID;
    }
    else {
	double	encodedTime =
	    ut_encode_time(year, month, day, hour, minute, second);

	*value = cv_convert_double(converter, encodedTime);
	cv_free(converter);
    }
    return status;
}

static ut_status
isTimeVisitBasic(
    const ut_unit*	unit,
    void*		arg)
{
    return (ut_status)ut_are_convertible(unit, second);
}

static ut_status
isTimeVisitProduct(
    const ut_unit*		unit,
    int				count,
    const ut_unit* const*	basicUnits,
    const int*			powers,
    void* 			arg)
{
    int	isTime;
    if (!ut_are_convertible(unit, second)) {
	isTime = 0;
    }
    else {
	int i;

	isTime = 0;
	for (i = 0; i < count; i++) {
	    if (ut_are_convertible(basicUnits[i], second) && powers[i] == 1) {
		isTime = 1;
		break;
	    }
	}
    }
    return (ut_status)isTime;
}

static ut_status
isTimeVisitGalilean(
    const ut_unit*	unit,
    double		scale,
    const ut_unit*	underlyingUnit,
    double		origin,
    void*		arg)
{
    return (ut_status)0;
}

static ut_status
isTimeVisitTimestamp(
    const ut_unit*	unit,
    const ut_unit*	timeUnit,
    double		origin,
    void*		arg)
{
    return (ut_status)1;
}

static ut_status
isTimeVisitLogarithmic(
    const ut_unit*	unit,
    double		base,
    const ut_unit*	reference,
    void*		arg)
{
    return (ut_status)0;
}

/*
 * Indicate if a unit structure refers to a unit of time.
 */
int
utIsTime(
    const utUnit	*up)
{
    ut_visitor	visitor;
    visitor.visit_basic = isTimeVisitBasic;
    visitor.visit_product = isTimeVisitProduct;
    visitor.visit_galilean = isTimeVisitGalilean;
    visitor.visit_timestamp = isTimeVisitTimestamp;
    visitor.visit_logarithmic = isTimeVisitLogarithmic;
    return ut_accept_visitor(up->unit2, &visitor, NULL);
}

static ut_status
hasOriginVisitBasic(
    const ut_unit*	unit,
    void*		arg)
{
    return (ut_status)0;
}

static ut_status
hasOriginVisitProduct(
    const ut_unit*		unit,
    int				count,
    const ut_unit* const*	basicUnits,
    const int*			powers,
    void* 			arg)
{
    return (ut_status)0;
}

static ut_status
hasOriginVisitGalilean(
    const ut_unit*	unit,
    double		scale,
    const ut_unit*	underlyingUnit,
    double		origin,
    void*		arg)
{
    return (ut_status)1;
}

static ut_status
hasOriginVisitTimestamp(
    const ut_unit*	unit,
    const ut_unit*	timeUnit,
    double		origin,
    void*		arg)
{
    return (ut_status)1;
}

static ut_status
hasOriginVisitLogarithmic(
    const ut_unit*	unit,
    double		base,
    const ut_unit*	reference,
    void*		arg)
{
    return (ut_status)0;
}

/*
 * Indicate if a unit structure has an origin.
 */
int
utHasOrigin(
    const utUnit	*up)
{
    ut_visitor	visitor;
    visitor.visit_basic = hasOriginVisitBasic;
    visitor.visit_product = hasOriginVisitProduct;
    visitor.visit_galilean = hasOriginVisitGalilean;
    visitor.visit_timestamp = hasOriginVisitTimestamp;
    visitor.visit_logarithmic = hasOriginVisitLogarithmic;
    return ut_accept_visitor(up->unit2, &visitor, NULL);
}

static utUnit*
resultingUnit(
    utUnit*		result,
    ut_unit* const	unit2)
{
    if (unit2 == NULL) {
	result = NULL;
    }
    else if (result != NULL) {
	if (setUnit(result, unit2) != 0) {
	    result = NULL;
	}
    }
    return result;
}

/*
 * Clear a unit structure.
 */
utUnit*
utClear(
    utUnit	*unit)
{
    return resultingUnit(unit, ut_get_dimensionless_unit_one(unitSystem));
}

/*
 * Copy a unit-structure.
 */
utUnit*
utCopy(
    const utUnit	*source,
    utUnit		*dest)
{
    return source == NULL
	? NULL
	: dest == NULL
	    ? NULL
	    : resultingUnit(dest, ut_clone(source->unit2));
}

/*
 * Multiply one unit-structure by another.
 */
utUnit*
utMultiply(
    const utUnit	*term1,
    const utUnit	*term2, 
    utUnit		*result)
{
    return term1 == NULL || term2 == NULL
	? NULL
	: resultingUnit(result, ut_multiply(term1->unit2, term2->unit2));
}

/*
 * Divide one unit-structure by another.
 */
utUnit*
utDivide(
    const utUnit	*numer,
    const utUnit	*denom, 
    utUnit		*result)
{
    return numer == NULL || denom == NULL
	? NULL
	: resultingUnit(result, ut_divide(numer->unit2, denom->unit2));
}

/*
 * Form the reciprocal of a unit-structure.
 */
utUnit*
utInvert(
    const utUnit	*unit,
    utUnit		*result)
{
    return unit == NULL ? NULL : resultingUnit(result, ut_invert(unit->unit2));
}

/*
 * Raise a unit-structure to a power.
 */
utUnit*
utRaise(
    const utUnit	*unit,
    int			power,
    utUnit		*result)
{
    return unit == NULL
	? NULL
	: resultingUnit(result, ut_raise(unit->unit2, power));
}

/*
 * Shift the origin of a unit-structure by an arithmetic amount.
 */
utUnit*
utShift(
    const utUnit	*unit,
    double		amount,
    utUnit		*result)
{
    return unit == NULL
	? NULL
	: resultingUnit(result, ut_offset(unit->unit2, amount));
}

/*
 * Scale a unit-structure.
 */
utUnit*
utScale(
    const utUnit	*unit,
    double		factor,
    utUnit		*result)
{
    return unit == NULL
	? NULL
	: resultingUnit(result, ut_scale(factor, unit->unit2));
}

/*
 * Compute the conversion factor between two unit-structures.
 */
int
utConvert(
    const utUnit	*from,
    const utUnit	*to,
    double		*slope,
    double		*intercept)
{
    int			status;
    cv_converter*	converter = ut_get_converter(from->unit2, to->unit2);

    if (converter == NULL) {
	status = ut_get_status();

	if (status == UT_BAD_ARG) {
	    status = UT_EINVALID;
	}
	else if (status == UT_NOT_SAME_SYSTEM) {
	    status = UT_ENOINIT;
	}
	else if (status == UT_MEANINGLESS) {
	    status = UT_ECONVERT;
	}
	else {
	    status = UT_EALLOC;
	}
    }
    else {
	*intercept = cv_convert_double(converter, 0.0);
	*slope = cv_convert_double(converter, 1.0) - *intercept;
	status = 0;
    }
    return status;
}

/*
 * Encode a unit-structure into a formatted unit-specification.
 */
int
utPrint(
    const utUnit	*unit,
    char		**buf)
{
    int	status;

    for (;;) {
	int	len = ut_format(unit->unit2, buffer, buflen, UT_ASCII);
	if (len == -1) {
	    status = ut_get_status();

	    if (status == UT_BAD_ARG) {
		status = UT_EINVALID;
	    }
	    else {
		status = UT_EALLOC;
	    }
	    break;
	}
	if (len < buflen) {
	    *buf = buffer;
	    status = 0;
	    break;
	}
	else {
	    int		newLen = buflen * 2;
	    char*	newBuf = malloc(newLen);

	    if (newBuf == NULL) {
		status = UT_EALLOC;
		break;
	    }
	    buffer = newBuf;
	    buflen = newLen;
	}
    }
    return status;
}

/*
 * Add a unit to the units database.
 */
int
utAdd(
    char		*name,
    int			hasPlural,
    const utUnit	*unit)
{
    int	status = ut_map_name_to_unit(name, UT_ASCII, unit->unit2);

    if (status == UT_SUCCESS) {
	status = ut_map_unit_to_name(unit->unit2, name, UT_ASCII);
	if (status == UT_SUCCESS) {
	    if (!hasPlural) {
		status = UT_SUCCESS;
	    }
	    else {
		extern const char*	ut_form_plural(const char*);
		const char*	plural = ut_form_plural(name);

		status = ut_map_name_to_unit(plural, UT_ASCII, unit->unit2);
	    }				/* unit has plural name */
	    if (status != UT_SUCCESS) {
		(void)ut_unmap_unit_to_name(unit->unit2, UT_ASCII);
	    }
	}				/* unit mapped to name */
	if (status != UT_SUCCESS) {
	    (void)ut_unmap_name_to_unit(unitSystem, name, UT_ASCII);
	}
    }					/* singular name mapped to unit */
    return status == UT_SUCCESS
	? 0
	: status == UT_EXISTS
	    ? UT_DUP
	    : UT_EALLOC;
}

/*
 * Return the unit corresponding to a unit-specification.
 *
 */
int
utFind(
    char	*spec,
    utUnit	*up)
{
    int		status;
    ut_unit*	unit = ut_parse(unitSystem, spec, UT_ASCII);

    if (unit == NULL) {
	status = ut_get_status();

	if (status == UT_BAD_ARG) {
	    status = UT_EINVALID;
	}
	else if (status == UT_SYNTAX) {
	    status = UT_ESYNTAX;
	}
	else if (status == UT_UNKNOWN) {
	    status = UT_EUNKNOWN;
	}
	else if (status == UT_OS) {
	    status = UT_EALLOC;
	}
    }
    else {
	status = setUnit(up, unit);
    }
    return status;
}

/*
 *	Terminate use of this package.
 */
void
utTerm()
{
    ut_free(second);
    second = NULL;
    ut_free(encodedTimeUnit);
    encodedTimeUnit = NULL;
    ut_free_system(unitSystem);
    unitSystem = NULL;
}
