/* ====================================================================
 * Copyright (c) 1995-1999 The Apache Group.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE APACHE GROUP OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Group and was originally based
 * on public domain software written at the National Center for
 * Supercomputing Applications, University of Illinois, Urbana-Champaign.
 * For more information on the Apache Group and the Apache HTTP server
 * project, please see <http://www.apache.org/>.
 *
 */
/*
 * cronoutils -- utilities for the cronolog program
 *
 * Copyright (c) 1996-1999 by Ford & Mason Ltd
 *
 * This software was submitted by Ford & Mason Ltd to the Apache
 * Software Foundation in December 1999.  Future revisions and
 * derivatives of this source code must acknowledge Ford & Mason Ltd
 * as the original contributor of this module.  All other licensing
 * and usage conditions are those of the Apache Software Foundation.
 *
 * Originally written by Andrew Ford <A.Ford@ford-mason.co.uk>
 *
 */

#include "hphp/util/cronoutils.h"
#include "hphp/util/portability.h"

#ifdef _MSC_VER
#include <direct.h>
#endif

extern char *tzname[2];

#ifndef DIR_MODE
#define DIR_MODE        ( S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH )
#endif

/* Constants for seconds per minute, hour, day and week */
#define SECS_PER_MIN            60
#define SECS_PER_HOUR           (60 * SECS_PER_MIN)
#ifndef SECS_PER_DAY
#define SECS_PER_DAY            (24 * SECS_PER_HOUR)
#endif
#define SECS_PER_WEEK           (7  * SECS_PER_DAY)

/* Allowances for daylight saving time changes and leap second.
 *  * Used for calculating the start of the next period.
 *   * (does Unix actually know about leap seconds?)
 *    */

#define LEAP_SECOND_ALLOWANCE   2
#define DST_ALLOWANCE           (3 * SECS_PER_HOUR + LEAP_SECOND_ALLOWANCE)

/* debug_file is the file to output debug messages to.  No debug
 * messages are output if it is set to NULL.
 */
FILE	*debug_file = nullptr;


/* America and Europe disagree on whether weeks start on Sunday or
 * Monday - weeks_start_on_mondays is set if a %U specifier is encountered.
 */
int	weeks_start_on_mondays = 0;


/* periods[] is an array of the names of the periods.
 */
const char	*periods[] =
{
    "second",
    "minute",
    "hour",
    "day",
    "week",
    "month",
    "year",
    "aeon"	/* i.e. once only */
};

/* period_seconds[] is an array of the number of seconds in a period.
 */
int	period_seconds[] =
{
    1,
    60,
    60 * 60,
    60 * 60 * 24,
    60 * 60 * 24 * 7,
    60 * 60 * 24 * 31,
    60 * 60 * 24 * 365
};

/* Try to create missing directories on the path of filename.
 *
 * Note that on a busy server there may theoretically be many cronolog
 * processes trying simultaneously to create the same subdirectories
 * so ignore any EEXIST errors on mkdir -- they probably just mean
 * that another process got there first.
 *
 * Unless CHECK_ALL_PREFIX_DIRS is defined, we save the directory of
 * the last file tested -- any common prefix should exist.  This
 * probably only saves a few stat system calls at the start of each
 * log period, but it might as well be done.
 */
void
create_subdirs(char *filename)
{
#ifndef CHECK_ALL_PREFIX_DIRS
    static char	lastpath[PATH_MAX] = "";
#endif
    struct stat stat_buf;
    char	dirname[PATH_MAX];
    char	*p;

    CRONO_DEBUG(("Creating missing components of \"%s\"\n", filename));
    for (p = filename; (p = strchr(p, '/')); p++)
    {
	if (p == filename)
	{
	    continue;	    /* Don't bother with the root directory */
	}

	memcpy(dirname, filename, p - filename);
	dirname[p-filename] = '\0';

#ifndef CHECK_ALL_PREFIX_DIRS
	if (strncmp(dirname, lastpath, strlen(dirname)) == 0)
	{
	    CRONO_DEBUG(("Initial prefix \"%s\" known to exist\n", dirname));
	    continue;
	}
#endif

	CRONO_DEBUG(("Testing directory \"%s\"\n", dirname));
	if (stat(dirname, &stat_buf) < 0)
	{
	    if (errno != ENOENT)
	    {
		perror(dirname);
		return;
	    }
	    else
	    {
		CRONO_DEBUG(("Directory \"%s\" does not exist -- creating\n", dirname));
                if ((mkdir(dirname, DIR_MODE) < 0) && (errno != EEXIST))
                {
		    perror(dirname);
		    return;
		}
	    }
	}
    }
#ifndef CHECK_ALL_PREFIX_DIRS
    strcpy(lastpath, dirname);
#endif
}

/* Create a hard or symbolic link to a filename according to the type specified.
 *
 * This function could do with more error checking!
 */
void
create_link(const char *pfilename,
	    const char *linkname, mode_t linktype,
	    const char *prevlinkname)
{
#ifndef _MSC_VER
    struct stat		stat_buf;

    if (lstat(prevlinkname, &stat_buf) == 0)
    {
	unlink(prevlinkname);
    }
    if (lstat(linkname, &stat_buf) == 0)
    {
	if (prevlinkname) {
	    rename(linkname, prevlinkname);
	}
	else {
	    unlink(linkname);
	}
    }
    if (linktype == S_IFLNK)
    {
	if (symlink(pfilename, linkname) < 0) {
          fprintf(stderr, "Creating link from %s to %s failed", pfilename, linkname);
        }
    }
    else
    {
	if (link(pfilename, linkname) < 0) {
          fprintf(stderr, "Creating link from %s to %s failed", pfilename, linkname);
        }
    }
#else
    fprintf(stderr, "Creating link from %s to %s not supported",
            pfilename, linkname);
#endif
}

/* Examine the log file name specifier for strftime conversion
 * specifiers and determine the period between log files.
 * Smallest period allowed is per minute.
 */
PERIODICITY
determine_periodicity(char *spec)
{
    PERIODICITY	periodicity = ONCE_ONLY;
    char 	ch;

    CRONO_DEBUG(("Determining periodicity of \"%s\"\n", spec));
    while ((ch = *spec++) != 0)
    {
	if (ch == '%')
	{
	    ch = *spec++;
	    if (!ch) break;

	    switch (ch)
	    {
	    case 'y':		/* two digit year */
	    case 'Y':		/* four digit year */
		if (periodicity > YEARLY)
		{
		    CRONO_DEBUG(("%%%c -> yearly\n", ch));
		    periodicity = YEARLY;
		}
		break;

	    case 'b':		/* abbreviated month name */
	    case 'h':		/* abbreviated month name (non-standard) */
	    case 'B':		/* full month name */
	    case 'm':		/* month as two digit number (with
				   leading zero) */
		if (periodicity > MONTHLY)
		{
		    CRONO_DEBUG(("%%%c -> monthly\n", ch));
		    periodicity = MONTHLY;
		}
  	        break;

	    case 'U':		/* week number (weeks start on Sunday) */
	    case 'W':		/* week number (weeks start on Monday) */
	        if (periodicity > WEEKLY)
		{
		    CRONO_DEBUG(("%%%c -> weeky\n", ch));
		    periodicity = WEEKLY;
		    weeks_start_on_mondays = (ch == 'W');
		}
		break;

	    case 'a':		/* abbreviated weekday name */
	    case 'A':		/* full weekday name */
	    case 'd':		/* day of the month (with leading zero) */
	    case 'e':		/* day of the month (with leading space -- non-standard) */
	    case 'j':		/* day of the year (with leading zeroes) */
	    case 'w':		/* day of the week (0-6) */
	    case 'D':		/* full date spec (non-standard) */
	    case 'x':		/* full date spec */
	        if (periodicity > DAILY)
		{
		    CRONO_DEBUG(("%%%c -> daily\n", ch));
		    periodicity = DAILY;
		}
  	        break;

	    case 'H':		/* hour (24 hour clock) */
	    case 'I':		/* hour (12 hour clock) */
	    case 'p':		/* AM/PM indicator */
	        if (periodicity > HOURLY)
		{
		    CRONO_DEBUG(("%%%c -> hourly\n", ch));
		    periodicity = HOURLY;
		}
		break;

	    case 'M':		/* minute */
	        if (periodicity > PER_MINUTE)
		{
		    CRONO_DEBUG(("%%%c -> per minute\n", ch));
		    periodicity = PER_MINUTE;
		}
		break;

	    case 'S':		/* second */
	    case 's':		/* seconds since the epoch (GNU non-standard) */
	    case 'c':		/* full time and date spec */
	    case 'T':		/* full time spec */
	    case 'r':		/* full time spec (non-standard) */
	    case 'R':		/* full time spec (non-standard) */
		CRONO_DEBUG(("%%%c -> per second", ch));
		periodicity = PER_SECOND;

	    default:		/* ignore anything else */
		CRONO_DEBUG(("ignoring %%%c\n", ch));
	        break;
	    }
	}
    }
    return periodicity;
}

/*
 */
PERIODICITY
parse_timespec(char *optarg, int *p_period_multiple)
{
    PERIODICITY		periodicity	= INVALID_PERIOD;
    int			period_multiple = 1;
    char 		*p = optarg;

    /* Skip leading whitespace */

    while (isspace(*p)) { p++; }


    /* Parse a digit string */

    if (isdigit(*p)) {
	period_multiple = *p++ - '0';

	while (isdigit(*p)) {
	    period_multiple *= 10;
	    period_multiple += (*p++ - '0');
	}
    }


    /* Skip whitespace */

    while (isspace(*p)) { p++; }

    if (strncasecmp(p, "sec", 3) == 0) {
	if (period_multiple < 60) {
	    periodicity = PER_SECOND;
	}
    }
    else if (strncasecmp(p, "min", 3) == 0) {
	if (period_multiple < 60) {
	    periodicity = PER_MINUTE;
	}
    }
    else if (strncasecmp(p, "hour", 4) == 0) {
	if (period_multiple < 24) {
	    periodicity = HOURLY;
	}
    }
    else if (strncasecmp(p, "day", 3) == 0) {
	if (period_multiple <= 31) {
	    periodicity = DAILY;
	}
    }
    else if (strncasecmp(p, "week", 4) == 0) {
	if (period_multiple < 53) {
	    periodicity = WEEKLY;
	}
    }
    else if (strncasecmp(p, "mon", 3) == 0) {
	if (period_multiple <= 12) {
	    periodicity = MONTHLY;
	}
    }
    *p_period_multiple = period_multiple;
    return periodicity;
}

/* To determine the time of the start of the next period add just
 * enough to move beyond the start of the next period and then
 * determine the time of the start of that period.
 *
 * There is a potential problem if the start or end of daylight saving
 * time occurs during the current period.
 */
time_t
start_of_next_period(time_t time_now, PERIODICITY periodicity, int period_multiple)
{
    time_t	start_time;

    switch (periodicity)
    {
    case YEARLY:
	start_time = (time_now + 366 * SECS_PER_DAY + DST_ALLOWANCE);
	break;

    case MONTHLY:
	start_time = (time_now + 31 * SECS_PER_DAY + DST_ALLOWANCE);
	break;

    case WEEKLY:
	start_time = (time_now + SECS_PER_WEEK + DST_ALLOWANCE);
	break;

    case DAILY:
	start_time = (time_now + SECS_PER_DAY + DST_ALLOWANCE);
	break;

    case HOURLY:
	start_time = time_now + period_multiple * SECS_PER_HOUR + LEAP_SECOND_ALLOWANCE;
	break;

    case PER_MINUTE:
	start_time = time_now + period_multiple * SECS_PER_MIN + LEAP_SECOND_ALLOWANCE;
	break;

    case PER_SECOND:
	start_time = time_now + 1;
	break;

    default:
	start_time = FAR_DISTANT_FUTURE;
	break;
    }
    return start_of_this_period(start_time, periodicity, period_multiple);
}

/* Determine the time of the start of the period containing a given time.
 * Break down the time with localtime and subtract the number of
 * seconds since the start of the period.  If the length of period is
 * equal or longer than a day then we have to check tht the
 * calculation is not thrown out by the start or end of daylight
 * saving time.
 */
time_t
start_of_this_period(time_t start_time, PERIODICITY periodicity, int period_multiple)
{
    struct tm	tm_initial;
    struct tm	tm_adjusted;
    int		expected_mday;

#ifndef _WIN32
    localtime_r(&start_time, &tm_initial);
#else
    struct tm * tempTime;

    tempTime = localtime(&start_time);
    if (nullptr != tempTime)
    {
        memcpy(&tm_initial, tempTime, sizeof(struct tm));

        free(tempTime);
        tempTime = nullptr;
    }
#endif
    switch (periodicity)
    {
    case YEARLY:
    case MONTHLY:
    case WEEKLY:
    case DAILY:
	switch (periodicity)
	{
	case YEARLY:
	    start_time -= (  (tm_initial.tm_yday * SECS_PER_DAY)
			   + (tm_initial.tm_hour * SECS_PER_HOUR)
			   + (tm_initial.tm_min  * SECS_PER_MIN)
			   + (tm_initial.tm_sec));
	    expected_mday = 1;
	    break;

	case MONTHLY:
	    start_time -= (  ((tm_initial.tm_mday - 1) * SECS_PER_DAY)
			   + ( tm_initial.tm_hour      * SECS_PER_HOUR)
			   + ( tm_initial.tm_min       * SECS_PER_MIN)
			   + ( tm_initial.tm_sec));
	    expected_mday = 1;
	    break;

	case WEEKLY:
	    if (weeks_start_on_mondays)
	    {
		tm_initial.tm_wday = (6 + tm_initial.tm_wday) % 7;
	    }
	    start_time -= (  (tm_initial.tm_wday * SECS_PER_DAY)
			   + (tm_initial.tm_hour * SECS_PER_HOUR)
			   + (tm_initial.tm_min  * SECS_PER_MIN)
			   + (tm_initial.tm_sec));
	    expected_mday = tm_initial.tm_mday;
	    break;

	case DAILY:
	    start_time -= (  (tm_initial.tm_hour * SECS_PER_HOUR)
			   + (tm_initial.tm_min  * SECS_PER_MIN )
			   +  tm_initial.tm_sec);
	    expected_mday = tm_initial.tm_mday;
	    break;

	default:
	    fprintf(stderr, "software fault in start_of_this_period()\n");
	    exit(1);
	}

	/* If the time of day is not equal to midnight then we need to
	 * adjust for daylight saving time.  Adjust the time backwards
	 * by the value of the hour, minute and second fields.  If the
	 * day of the month is not as expected one then we must have
	 * adjusted back to the previous day so add 24 hours worth of
	 * seconds.
	 */
#ifndef _WIN32
	localtime_r(&start_time, &tm_adjusted);
#else
	tempTime = localtime(&start_time);
	if (nullptr != tempTime)
	{
	    memcpy(&tm_adjusted, tempTime, sizeof(struct tm));

	    free(tempTime);
	    tempTime = nullptr;
	}
#endif
	if (   (tm_adjusted.tm_hour != 0)
	    || (tm_adjusted.tm_min  != 0)
	    || (tm_adjusted.tm_sec  != 0))
	{
	    char	sign   = '-';
	    time_t      adjust = - (  (tm_adjusted.tm_hour * SECS_PER_HOUR)
				    + (tm_adjusted.tm_min  * SECS_PER_MIN)
				    + (tm_adjusted.tm_sec));

	    if (tm_adjusted.tm_mday != expected_mday)
	    {
		adjust += SECS_PER_DAY;
		sign = '+';
	    }
	    start_time += adjust;

	    if (adjust < 0)
	    {
		adjust = -adjust;
	    }

	    CRONO_DEBUG(("Adjust for dst: %02d/%02d/%04d %02d:%02d:%02d -- %c%0d:%02d:%02d\n",
		   tm_initial.tm_mday, tm_initial.tm_mon+1, tm_initial.tm_year+1900,
		   tm_initial.tm_hour, tm_initial.tm_min,   tm_initial.tm_sec, sign,
		   adjust / SECS_PER_HOUR, (adjust / 60) % 60, adjust % SECS_PER_HOUR));
	}
	break;

    case HOURLY:
	start_time -= (tm_initial.tm_sec + tm_initial.tm_min * SECS_PER_MIN);
	if (period_multiple > 1) {
	    start_time -= SECS_PER_HOUR * (tm_initial.tm_hour -
					   period_multiple * (tm_initial.tm_hour / period_multiple));
	}
	break;

    case PER_MINUTE:
	start_time -= tm_initial.tm_sec;
	if (period_multiple > 1) {
	    start_time -= SECS_PER_MIN * (tm_initial.tm_min -
					  period_multiple * (tm_initial.tm_min / period_multiple));
	}
	break;

    case PER_SECOND:	/* No adjustment needed */
    default:
	break;
    }
    return start_time;
}

/* Converts struct tm to time_t, assuming the data in tm is UTC rather
 * than local timezone (as mktime assumes).
 *
 * Contributed by Roger Beeman <beeman@cisco.com>.
 */
time_t
mktime_from_utc(struct tm *t)
{
   time_t tl, tb;

   tl = mktime(t);
   tb = mktime(gmtime(&tl));
   return (tl <= tb ? (tl + (tl - tb)) : (tl - (tb - tl)));
}

/* Check whether the string is processed well.  It is processed if the
 * pointer is non-NULL, and it is either at the `GMT', or at the end
 * of the string.
 */
static int
check_end(const char *p)
{
   if (!p)
      return 0;
   while (isspace(*p))
      ++p;
   if (!*p || (p[0] == 'G' && p[1] == 'M' && p[2] == 'T'))
      return 1;
   else
      return 0;
}

/* NOTE: We don't use `%n' for white space, as OSF's strptime uses
   it to eat all white space up to (and including) a newline, and
   the function fails (!) if there is no newline.

   Let's hope all strptime-s use ` ' to skipp *all* whitespace
   instead of just one (it works that way on all the systems I've
   tested it on). */

static const char *european_date_formats[] =
{
    "%d %b %Y %T",       	/* dd mmm yyyy HH:MM:SS */
    "%d %b %Y %H:%M",       	/* dd mmm yyyy HH:MM    */
    "%d %b %Y",       		/* dd mmm yyyy          */
    "%d-%b-%Y %T",       	/* dd-mmm-yyyy HH:MM:SS */
    "%d-%b-%Y %H:%M",       	/* dd-mmm-yyyy HH:MM    */
    "%d-%b-%y %T",       	/* dd-mmm-yy   HH:MM:SS */
    "%d-%b-%y %H:%M",  		/* dd-mmm-yy   HH:MM    */
    "%d-%b-%Y",
    "%b %d %T %Y",
    "%b %d %Y",
    nullptr
};

static const char *american_date_formats[] =
{
    "%b %d %Y %T",       	/* dd mmm yyyy HH:MM:SS */
    "%b %d %Y %H:%M",       	/* dd mmm yyyy HH:MM    */
    "%b %d %Y",       		/* dd mmm yyyy          */
    "%b-%d-%Y %T",       	/* dd-mmm-yyyy HH:MM:SS */
    "%b-%d-%Y %H:%M",       	/* dd-mmm-yyyy HH:MM    */
    "%b-%d-%Y",
    "%b/%d/%Y %T",
    "%b/%d/%Y %H:%M",
    "%b/%d/%Y",
    nullptr
};



time_t
parse_time(char *time_str, int use_american_date_formats)
{
   struct tm    tm;
   const char		**date_formats;

   memset(&tm, 0, sizeof (tm));
   tm.tm_isdst = -1;


   for (date_formats = (use_american_date_formats
			? american_date_formats
			: european_date_formats);
	*date_formats;
	date_formats++)
   {
       if (check_end((const char *)strptime(time_str, *date_formats, &tm)))
	   return mktime_from_utc(&tm);
   }

   return -1;
}



/* Simple debugging print function.
 */
void
print_debug_msg(const char *msg, ...)
{
    va_list	ap;

    va_start(ap, msg);
    vfprintf(debug_file, msg, ap);
}


/* Build a timestamp and return a pointer to it.
 * (has a number of static buffers that are rotated).
 */
char *
timestamp(time_t thetime)
{
    static int	index = 0;
    static char	buffer[4][80];
    struct tm	*tm;
    char	*retval;

    retval = buffer[index++];
    index %= 4;

    tm = localtime(&thetime);
    strftime(retval, 80, "%Y/%m/%d-%H:%M:%S %Z", tm);
    return retval;
}
