/*
** Zabbix
** Copyright (C) 2001-2022 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

#include "common.h"

static void	tm_add(struct tm *tm, int multiplier, zbx_time_unit_t base);
static void	tm_sub(struct tm *tm, int multiplier, zbx_time_unit_t base);

static int	time_unit_seconds[ZBX_TIME_UNIT_COUNT] = {0, 1, SEC_PER_MIN, SEC_PER_HOUR, SEC_PER_DAY, SEC_PER_WEEK, 0,
		0, 0};

zbx_time_unit_t	zbx_tm_str_to_unit(const char *text)
{
	switch (*text)
	{
		case 's':
			return ZBX_TIME_UNIT_SECOND;
		case 'm':
			return ZBX_TIME_UNIT_MINUTE;
		case 'h':
			return ZBX_TIME_UNIT_HOUR;
		case 'd':
			return ZBX_TIME_UNIT_DAY;
		case 'w':
			return ZBX_TIME_UNIT_WEEK;
		case 'M':
			return ZBX_TIME_UNIT_MONTH;
		case 'y':
			return ZBX_TIME_UNIT_YEAR;
		default:
			return ZBX_TIME_UNIT_UNKNOWN;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: parse time period in format <multiplier><time unit>               *
 *                                                                            *
 * Parameters: period     - [IN] the time period                              *
 *             len        - [OUT] the length of parsed time period            *
 *             multiplier - [OUT] the parsed multiplier                       *
 *             base       - [OUT] the parsed time unit                        *
 *             error      - [OUT] the error message if parsing failed         *
 *                                                                            *
 * Return value: SUCCEED - period was parsed successfully                     *
 *               FAIL    - invalid time period was specified                  *
 *                                                                            *
 ******************************************************************************/
int	zbx_tm_parse_period(const char *period, size_t *len, int *multiplier, zbx_time_unit_t *base, char **error)
{
	const char	*ptr;

	for (ptr = period; 0 != isdigit(*ptr); ptr++)
		;

	if (FAIL == is_uint_n_range(period, (size_t)(ptr - period), multiplier, sizeof(*multiplier), 0, UINT32_MAX))
	{
		*error = zbx_strdup(*error, "invalid period multiplier");
		return FAIL;
	}

	if (ZBX_TIME_UNIT_UNKNOWN == (*base = zbx_tm_str_to_unit(ptr)))
	{
		*error = zbx_strdup(*error, "invalid period time unit");
		return FAIL;
	}

	*len = (size_t)(ptr - period) + 1;

	return SUCCEED;
}

/******************************************************************************
 *                                                                            *
 * Purpose: add seconds to the time and adjust result by dst                  *
 *                                                                            *
 * Parameter: tm      - [IN/OUT] the time structure                           *
 *            seconds - [IN] the seconds to add (can be negative)             *
 *                                                                            *
 ******************************************************************************/
static void	tm_add_seconds(struct tm *tm, int seconds)
{
	time_t		time_new;
	struct tm	tm_new = *tm;

	if (-1 == (time_new = mktime(&tm_new)))
	{
		THIS_SHOULD_NEVER_HAPPEN;
		return;
	}

	time_new += seconds;
	localtime_r(&time_new, &tm_new);

	if (tm->tm_isdst != tm_new.tm_isdst && -1 != tm->tm_isdst && -1 != tm_new.tm_isdst)
	{
		if (0 == tm_new.tm_isdst)
			tm_add(&tm_new, 1, ZBX_TIME_UNIT_HOUR);
		else
			tm_sub(&tm_new, 1, ZBX_TIME_UNIT_HOUR);
	}

	*tm = tm_new;
}

/******************************************************************************
 *                                                                            *
 * Purpose: add time duration without adjusting DST clocks                    *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            multiplier - [IN] the unit multiplier                           *
 *            base       - [IN] the time unit to add                          *
 *                                                                            *
 ******************************************************************************/
static void	tm_add(struct tm *tm, int multiplier, zbx_time_unit_t base)
{
	int	shift;

	switch (base)
	{
		case ZBX_TIME_UNIT_HOUR:
			tm->tm_hour += multiplier;
			if (24 <= tm->tm_hour)
			{
				shift = tm->tm_hour / 24;
				tm->tm_hour %= 24;
				tm_add(tm, shift, ZBX_TIME_UNIT_DAY);
			}
			break;
		case ZBX_TIME_UNIT_DAY:
			tm->tm_mday += multiplier;
			while (tm->tm_mday > (shift = zbx_day_in_month(tm->tm_year + 1900, tm->tm_mon + 1)))
			{
				tm->tm_mday -= shift;
				tm_add(tm, 1, ZBX_TIME_UNIT_MONTH);
			}
			tm->tm_wday += multiplier;
			tm->tm_wday %= 7;
			break;
		case ZBX_TIME_UNIT_WEEK:
			tm_add(tm, multiplier * 7, ZBX_TIME_UNIT_DAY);
			break;
		case ZBX_TIME_UNIT_MONTH:
			tm->tm_mon += multiplier;
			if (12 <= tm->tm_mon)
			{
				shift = tm->tm_mon / 12;
				tm->tm_mon %= 12;
				tm_add(tm, shift, ZBX_TIME_UNIT_YEAR);
			}
			break;
		case ZBX_TIME_UNIT_YEAR:
			tm->tm_year += multiplier;
			break;
		default:
			break;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: add time duration                                                 *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            multiplier - [IN] the unit multiplier                           *
 *            base       - [IN] the time unit to add                          *
 *                                                                            *
 ******************************************************************************/
void	zbx_tm_add(struct tm *tm, int multiplier, zbx_time_unit_t base)
{
	if (ZBX_TIME_UNIT_MONTH == base || ZBX_TIME_UNIT_YEAR == base)
	{
		int	days_max;

		tm_add(tm, multiplier, base);

		days_max = zbx_day_in_month(tm->tm_year + 1900, tm->tm_mon + 1);
		if (tm->tm_mday > days_max)
			tm->tm_mday = days_max;
	}

	tm_add_seconds(tm, multiplier * time_unit_seconds[base]);

	return;
}

/******************************************************************************
 *                                                                            *
 * Purpose: convert negative number to positive by wrapping around the base   *
 *                                                                            *
 * Parameter: value - [IN/OUT] the value to convert                           *
 *            base  - [IN] the wrap base                                      *
 *                                                                            *
 ******************************************************************************/
static void	neg_to_pos_wrap(int *value, int base)
{
	int	reminder = *value % base;

	*value = (0 == reminder ? 0 : base + reminder);
}

/******************************************************************************
 *                                                                            *
 * Purpose: subtracts time duration without adjusting DST clocks              *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            multiplier - [IN] the unit multiplier                           *
 *            base       - [IN] the time unit to add                          *
 *                                                                            *
 ******************************************************************************/
static void	tm_sub(struct tm *tm, int multiplier, zbx_time_unit_t base)
{
	int	shift;

	switch (base)
	{
		case ZBX_TIME_UNIT_HOUR:
			tm->tm_hour -= multiplier;
			if (0 > tm->tm_hour)
			{
				shift = -tm->tm_hour / 24;
				neg_to_pos_wrap(&tm->tm_hour, 24);
				if (0 != tm->tm_hour)
					shift++;
				tm_sub(tm, shift, ZBX_TIME_UNIT_DAY);
			}
			return;
		case ZBX_TIME_UNIT_DAY:
			tm->tm_mday -= multiplier;
			while (0 >= tm->tm_mday)
			{
				int	prev_mon;

				if (0 > (prev_mon = tm->tm_mon - 1))
					prev_mon = 11;
				prev_mon++;

				tm->tm_mday += zbx_day_in_month(tm->tm_year + 1900, prev_mon);
				tm_sub(tm, 1, ZBX_TIME_UNIT_MONTH);
			}
			tm->tm_wday -= multiplier;
			if (0 > tm->tm_wday)
				neg_to_pos_wrap(&tm->tm_wday, 7);
			return;
		case ZBX_TIME_UNIT_WEEK:
			tm_sub(tm, multiplier * 7, ZBX_TIME_UNIT_DAY);
			return;
		case ZBX_TIME_UNIT_MONTH:
			tm->tm_mon -= multiplier;
			if (0 > tm->tm_mon)
			{
				shift = -tm->tm_mon / 12;
				neg_to_pos_wrap(&tm->tm_mon, 12);
				if (0 != tm->tm_mon)
					shift++;
				tm_sub(tm, shift, ZBX_TIME_UNIT_YEAR);
			}
			return;
		case ZBX_TIME_UNIT_YEAR:
			tm->tm_year -= multiplier;
			return;
		default:
			return;
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: subtracts time duration                                           *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            multiplier - [IN] the unit multiplier                           *
 *            base       - [IN] the time unit to add                          *
 *                                                                            *
 ******************************************************************************/
void	zbx_tm_sub(struct tm *tm, int multiplier, zbx_time_unit_t base)
{
	if (ZBX_TIME_UNIT_ISOYEAR == base)
	{
		int	week_num, total_weeks;

		week_num = zbx_get_week_number(tm);

		/* use zbx_tm_sub instead of tm_sub to force weekday recalculation */
		zbx_tm_sub(tm, week_num, ZBX_TIME_UNIT_WEEK);

		total_weeks = zbx_get_week_number(tm);
		if (week_num > total_weeks)
			week_num--;
		tm_sub(tm, zbx_get_week_number(tm) - week_num, ZBX_TIME_UNIT_WEEK);
	}
	else if (ZBX_TIME_UNIT_MONTH == base || ZBX_TIME_UNIT_YEAR == base)
	{
		int	days_max;

		tm_sub(tm, multiplier, base);

		days_max = zbx_day_in_month(tm->tm_year + 1900, tm->tm_mon + 1);
		if (tm->tm_mday > days_max)
			tm->tm_mday = days_max;
	}

	tm_add_seconds(tm, -multiplier * time_unit_seconds[base]);

	return;
}

/******************************************************************************
 *                                                                            *
 * Purpose: rounds time by the specified unit upwards                         *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            base       - [IN] the time unit                                 *
 *                                                                            *
 ******************************************************************************/
void	zbx_tm_round_up(struct tm *tm, zbx_time_unit_t base)
{
	if (0 != tm->tm_sec)
	{
		tm->tm_sec = 0;
		zbx_tm_add(tm, 1, ZBX_TIME_UNIT_MINUTE);
	}

	if (ZBX_TIME_UNIT_MINUTE == base)
		return;

	if (0 != tm->tm_min)
	{
		tm->tm_min = 0;
		zbx_tm_add(tm, 1, ZBX_TIME_UNIT_HOUR);
	}

	if (ZBX_TIME_UNIT_HOUR == base)
		return;

	if (0 != tm->tm_hour)
	{
		tm->tm_hour = 0;
		zbx_tm_add(tm, 1, ZBX_TIME_UNIT_DAY);
	}

	if (ZBX_TIME_UNIT_DAY == base)
		return;

	if (ZBX_TIME_UNIT_WEEK == base)
	{
		if (1 != tm->tm_wday)
		{
			zbx_tm_add(tm, (0 == tm->tm_wday ? 1 : 8 - tm->tm_wday), ZBX_TIME_UNIT_DAY);
			tm->tm_wday = 1;
		}
		return;
	}

	if (1 != tm->tm_mday)
	{
		tm->tm_mday = 1;
		zbx_tm_add(tm, 1, ZBX_TIME_UNIT_MONTH);
	}

	if (ZBX_TIME_UNIT_MONTH == base)
		return;

	if (0 != tm->tm_mon)
	{
		tm->tm_mon = 0;
		zbx_tm_add(tm, 1, ZBX_TIME_UNIT_YEAR);
	}

	return;
}

/******************************************************************************
 *                                                                            *
 * Purpose: rounds time by the specified unit downwards                       *
 *                                                                            *
 * Parameter: tm         - [IN/OUT] the time structure                        *
 *            base       - [IN] the time unit                                 *
 *                                                                            *
 ******************************************************************************/
void	zbx_tm_round_down(struct tm *tm, zbx_time_unit_t base)
{
	switch (base)
	{
		case ZBX_TIME_UNIT_WEEK:
			if (1 != tm->tm_wday)
			{
				zbx_tm_sub(tm, (0 == tm->tm_wday ? 6 : tm->tm_wday - 1), ZBX_TIME_UNIT_DAY);
				tm->tm_wday = 1;
			}

			tm->tm_hour = 0;
			tm->tm_min = 0;
			tm->tm_sec = 0;
			break;
		case ZBX_TIME_UNIT_ISOYEAR:
			zbx_tm_round_down(tm, ZBX_TIME_UNIT_WEEK);
			zbx_tm_sub(tm, zbx_get_week_number(tm) - 1, ZBX_TIME_UNIT_WEEK);
			break;
		case ZBX_TIME_UNIT_YEAR:
			tm->tm_mon = 0;
			ZBX_FALLTHROUGH;
		case ZBX_TIME_UNIT_MONTH:
			tm->tm_mday = 1;
			ZBX_FALLTHROUGH;
		case ZBX_TIME_UNIT_DAY:
			tm->tm_hour = 0;
			ZBX_FALLTHROUGH;
		case ZBX_TIME_UNIT_HOUR:
			tm->tm_min = 0;
			ZBX_FALLTHROUGH;
		case ZBX_TIME_UNIT_MINUTE:
			tm->tm_sec = 0;
			break;
		default:
			break;
	}

	tm_add_seconds(tm, 0);

	return;
}

const char	*zbx_timespec_str(const zbx_timespec_t *ts)
{
	static ZBX_THREAD_LOCAL char	str[32];

	time_t		ts_time = ts->sec;
	struct tm	tm;

	localtime_r(&ts_time, &tm);
	zbx_snprintf(str, sizeof(str), "%04d.%02d.%02d %02d:%02d:%02d.%09d", tm.tm_year + 1900, tm.tm_mon + 1,
			tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, ts->ns);

	return str;
}

static	int	get_week_days(int yday, int wday)
{
	return yday - (yday - wday + 382) % 7 + 3;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get ISO 8061 week number (1-53)                                   *
 *                                                                            *
 ******************************************************************************/
int	zbx_get_week_number(const struct tm *tm)
{
	int	days;

	if (0 > (days = get_week_days(tm->tm_yday, tm->tm_wday)))
	{
		int	d = tm->tm_yday + 365;

		if (SUCCEED == zbx_is_leap_year(tm->tm_year + 1899))
			d++;

		days = get_week_days(d, tm->tm_wday);
	}
	else
	{
		int days_next, d;

		d = tm->tm_yday - 365;
		if (SUCCEED == zbx_is_leap_year(tm->tm_year + 1900))
			d--;

		if (0 <= (days_next = get_week_days(d, tm->tm_wday)))
			days = days_next;
	}

	return days / 7 + 1;
}