//-----------------------------------------------------------------------------
// Copyright © 2006 - Philip Howard - All rights reserved
//
// 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//-----------------------------------------------------------------------------
// package	libh/time
// homepage	http://libh.slashusr.org/
//-----------------------------------------------------------------------------
// author	Philip Howard
// email	libh at ipal dot org
// homepage	http://phil.ipal.org/
//-----------------------------------------------------------------------------
// This file is best viewed using a fixed spaced font such as Courier
// and in a display at least 120 columns wide.
//-----------------------------------------------------------------------------
// reference	Explanatory Supplement to the Astronomical Almanac,
//		P. Kenneth Seidelmann, editor  [ISBN 0-935702-68-7]
//		http://shop.bn.com/bookSearch/isbnInquiry.asp?isbn=0935702687
//-----------------------------------------------------------------------------

#include <ctype.h>
#include <locale.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <libh/io.h>
#include <libh/time.h>
#include <libh/string.h>

//-----------------------------------------------------------------------------
// program	timecalc
//
// purpose	Perform simple calculations on absolute dates and times with
//		relative time intervals added or subtracted.
//
// command	timecalc  [options]  [absolutetime]  [relativetime ...]
//
// arguments	-f "format string"
//		This specifies the output format for the resulting absolute
//		date and time value.
//
//		YYYY-MM-DD.HH:MM:SS.fraction
//		This is an input of an absolute date and time value.  If the
//		time portion is omitted, the time at the beginning of the day
//		is used (e.g. all zero).
//
//		{+,-}[NNw][NNd][NNh][NNm][NNs]
//		This is an input of a relative interval of time to be added to
//		or subtracted from the absolute date and time value
//
// compile	gcc -O3 -o timecalc timecalc.c -lh
//		gcc -O3 -o timecalc timecalc.c /usr/local/lib/libh.a -lm
//-----------------------------------------------------------------------------


//-----------------------------------------------------------------------------
// function	convert_relative_time
//
// purpose	Convert a string with a relative time value expression to an
//		internal integer value.
//
// arguments	1 (const char *) string to convert
//		2 (unsigned long long *) where to store relative time
//
// returns	(int) -1 : error in conversion
//		(Int)  0 : conversion OK
//-----------------------------------------------------------------------------
static
int
convert_relative_time (
    const char *		arg_time_str
    ,
    unsigned long long *	arg_time_p
    )
{
    unsigned long long	accum_whole	;
    unsigned long long	accum_fract	;
    unsigned long long	whole		;
    unsigned long long	fract		;
    unsigned long long	scale		;
    unsigned long long	multi		;

    int			ch		;
    int			dig		;

    accum_whole = 0ULL;
    accum_fract = 0ULL;
    whole = 0ULL;
    fract = 0ULL;
    scale = 0ULL;
    multi = 0ULL;

    //--------------------------------------------------
    // Loop through relative time components to convert.
    //--------------------------------------------------
    for ( ; * arg_time_str ; ++ arg_time_str ) {
	ch = * arg_time_str;

	switch ( ch ) {

	//----------------------------------------------------
	// If this is a digit, convert and accumulate a value.
	//----------------------------------------------------
	case '0': case '1': case '2': case '3': case '4':
	case '5': case '6': case '7': case '8': case '9':
	    dig = ch - '0';

	    if ( scale ) {
		if ( scale > ( ( ~ (unsigned long long) 0 ) / 10ULL ) ) {
		    continue;
		}
		scale *= 10ULL;
		fract *= 10ULL;
		fract += dig;
	    } else {
		if ( whole > ( ( ( ~ (unsigned long long) 0 ) - dig ) / 10ULL ) ) {
		    return -1;
		}
		whole *= 10ULL;
		whole += dig;
	    }
	    continue;

	//------------------------------------------
	// If this starts a new fraction, set it up.
	//------------------------------------------
	case '.':
	case ',':
	    if ( scale ) return -1;
	    scale = 1ULL;
	    continue;

	//---------------------------------------------------
	// Determine the multiplier value from the character.
	//---------------------------------------------------
	case 'w': case 'W':
	    multi = 604800ULL;
	    break;
	case 'd': case 'D':
	    multi = 86400ULL;
	    break;
	case 'h': case 'H':
	    multi = 3600ULL;
	    break;
	case 'm': case 'M':
	    multi = 60ULL;
	    break;
	case 's': case 'S': case 0:
	    multi = 1ULL;
	    break;

	//---------------------------------
	// Anything else is simply invalid.
	//---------------------------------
	default:
	    return -1;

	}

	//------------------------------------------------------
	// If this was a multiplier, handle all such cases here.
	//------------------------------------------------------
	if ( multi ) {

	    //----------------------------
	    // Accumulate time components.
	    //----------------------------
	    if ( scale ) {
		//------------------------------------------
		// Make the fractional scale be nanoseconds.
		//------------------------------------------
		if ( scale > 1000000000ULL )	fract /= ( scale / 1000000000ULL );
		else if ( scale < 1000000000ULL )	fract *= ( 1000000000ULL / scale );
		accum_fract += fract * multi;
	    }
	    accum_whole += whole * multi;

	    //---------------------------
	    // Reset for a new component.
	    //---------------------------
	    whole = 0ULL;
	    fract = 0ULL;
	    scale = 0ULL;
	    multi = 0ULL;
	}
    }

    accum_fract += 500ULL;				// round fraction part from ...
    accum_fract /= 1000ULL;				// ... nanoseconds to microseconds
    accum_whole *= 1000000ULL;				// make whole be microseconds
    accum_whole += accum_fract;				// add parts together
    if ( arg_time_p ) * arg_time_p = accum_whole;	// store time value
    
    return 0;
}


//-----------------------------------------------------------------------------
// function	main
//-----------------------------------------------------------------------------
int
main (
    int		argc
    ,
    char * *	argv
)
{
    char * *		arg_ptr			;

    char *		format			;
    char *		opt_rel			;
    char *		opt_val		;
    char *		opt_str			;
    char *		pgm_name		;

    etime_t		abs_time		;

    int			abs_num_1		;
    int			abs_num_2		;
    int			arg_cnt			;
    int			arg_num			;
    int			opt_code		;
    int			opt_num_1		;
    int			opt_num_2		;

    static char		default_format	[]	= "%Y-%m-%d %H:%M:%S.%U";

    //-----------------------------
    // Do all initialization first.
    //-----------------------------
    pgm_name = str_last_part( argv[0], '/' );
    abs_num_1 = 0;
    abs_num_2 = 0;
    abs_time = 0;
    arg_num = 0;

    format = default_format;

    //----------------------------
    // Loop through all arguments.
    //----------------------------
    arg_ptr = argv;
    arg_cnt = argc;
    while ( ++ arg_ptr , -- arg_cnt > 0 && * arg_ptr ) {
	++ arg_num;
	opt_code = 0;
	opt_val = NULL;
	opt_rel = NULL;
	opt_num_1 = 0;
	opt_num_2 = 0;

	//-----------------------------------------------
	// Handle options beginning with '-' or '+' here.
	//-----------------------------------------------
	if ( arg_ptr[0][0] == '-' || arg_ptr[0][0] == '+' ) {
	    opt_str = arg_ptr[0];
	    opt_num_1 = arg_num;

	    //---------------------------------------------------------
	    // Check for standalone '-' or '+' for default add/subtract
	    // of a relative time in the next argument.
	    //---------------------------------------------------------
	    if ( arg_ptr[0][1] == 0 ) {
		opt_code = arg_ptr[0][0];
		++ arg_num;
		++ arg_ptr;
		-- arg_cnt;
		if ( arg_cnt > 0 && * arg_ptr ) {
		    opt_rel = arg_ptr[0];
		} else {
		    fprintf( stderr, "%s: bad option at [%u]: \"%s\"\n",
			     pgm_name, opt_num_1, argv[opt_num_1] );
		    return 1;
		}
	    }

	    //-------------------------------------------------------
	    // Check for '-' or '+' combined with a number which will
	    // be a relative time to add/subtract in same argument.
	    //-------------------------------------------------------
	    else if ( ( '0' <= arg_ptr[0][1] && arg_ptr[0][1] <= '9' )
		      || ( ( arg_ptr[0][1] == '.' || arg_ptr[0][1] == ',' ) &&
			  '0' <= arg_ptr[0][2] && arg_ptr[0][2] <= '9' ) ) {
		opt_code = arg_ptr[0][0];
		opt_rel = arg_ptr[0] + 1;
	    }

	    //---------------------------------------------------------
	    // If '-' or '+' followed by character and nothing else ...
	    //---------------------------------------------------------
	    else if ( arg_ptr[0][2] == 0 ) {

		//------------------------
		// Check for help request.
		//------------------------
		if ( arg_ptr[0][1] == 'h' ) goto help;

		//-------------------------------
		// Check for -c for current time.
		//-------------------------------
		if ( arg_ptr[0][1] == 'c' ) {

		    //-----------------------------------------------
		    // Only one absolute time/date value can be used.
		    // Check for excess.
		    //-----------------------------------------------
		    if ( abs_num_1 > 0 ) {
			fprintf( stderr, "%s: extra absolute time/date in argument [%u]: \"%s\"",
				 pgm_name, opt_num_1, argv[opt_num_1] );
			if ( opt_num_2 ) {
			    fprintf( stderr, " \"%s\"", argv[opt_num_2] );
			}
			fputc( '\n', stderr );
			fprintf( stderr, "%s: already have absolute time/date from [%u]: \"%s\"",
				 pgm_name, abs_num_1, argv[abs_num_1] );
			if ( abs_num_2 ) {
			    fprintf( stderr, " \"%s\"", argv[abs_num_2] );
			}
			fputc( '\n', stderr );
			return 1;
		    }
		    abs_time = current_etime();
		    abs_num_1 = arg_num;
		    continue;
		}

		//--------------------------------------------------------------
		// Check for any option that needs a value in the next argument.
		// Example: -g 2006/1/1
		//--------------------------------------------------------------
		opt_code = arg_ptr[0][1];
		++ arg_num;
		++ arg_ptr;
		-- arg_cnt;
		if ( arg_cnt > 0 && * arg_ptr ) {
		    opt_val = arg_ptr[0];
		    opt_num_2 = arg_num;
		} else {
		    fprintf( stderr, "%s: bad option at [%u]: \"%s\"\n",
			     pgm_name, opt_num_1, argv[opt_num_1] );
		    return 1;
		}
	    }

	    //-----------------------------------------
	    // Check for value following an equal sign.
	    // Example: -g=2006/1/1
	    //-----------------------------------------
	    else if ( arg_ptr[0][2] == '=' ) {
		opt_code = arg_ptr[0][1];
		opt_val = arg_ptr[0] + 3;
	    }
	}

	//--------------------------
	// Check for obvious format.
	//--------------------------
	else if ( arg_ptr[0][0] == '%' ) {
	    format = arg_ptr[0];
	    continue;
	}

	//--------------------------------------
	// Check for option in assignment style.
	// Example: g=2006/1/1
	//--------------------------------------
	else if ( arg_ptr[0][1] == '=' ) {
	    opt_code = arg_ptr[0][0];
	    opt_val = arg_ptr[0] + 2;
	    opt_num_1 = arg_num;
	}

	//-------------------------------------------
	// Check for a standalone absolute date/time.
	// Example: 2006/1/1
	//-------------------------------------------
	else if ( '0' < arg_ptr[0][0] && arg_ptr[0][0] <= '9' ) {
	    opt_code = 'g'; // Use Gregorian calendar as default.
	    opt_val = arg_ptr[0];
	    opt_num_1 = arg_num;
	}

	//-----------------------------------------------
	// If an option with value is given in some form,
	// then handle it here.
	//-----------------------------------------------
	if ( opt_val ) {

	    //------------------------------------------------
	    // Handle options that are not absolute date/time.
	    //------------------------------------------------
	    switch( opt_code ) {

	    case 'f':
		format = opt_val;
		continue;

	    }

	    //-----------------------------------------------
	    // Only one absolute time/date value can be used.
	    // Check for excess.
	    //-----------------------------------------------
	    if ( abs_num_1 > 0 ) {
		fprintf( stderr, "%s: extra absolute time/date in argument [%u]: \"%s\"",
			 pgm_name, opt_num_1, argv[opt_num_1] );
		if ( opt_num_2 ) {
		    fprintf( stderr, " \"%s\"", argv[opt_num_2] );
		}
		fputc( '\n', stderr );
		fprintf( stderr, "%s: already have absolute time/date from [%u]: \"%s\"",
			 pgm_name, abs_num_1, argv[abs_num_1] );
		if ( abs_num_2 ) {
		    fprintf( stderr, " \"%s\"", argv[abs_num_2] );
		}
		fputc( '\n', stderr );
		return 1;
	    }

	    //-------------------------------
	    // Remember this absolute option.
	    //-------------------------------
	    abs_num_1 = opt_num_1;
	    abs_num_2 = opt_num_2;

	    //-----------------------------------------------
	    // Handle option according to appropriate method.
	    //-----------------------------------------------
	    switch( opt_code ) {

	    case 'g':
                abs_time = str_gregorian_to_etime( opt_val );
		break;

	    case 'j':
                abs_time = str_julian_to_etime( opt_val );
		break;

	    case 'e':
		abs_time = eday_to_etime( str_expr_to_sl( opt_val, NULL, 0 ) );
		break;

	    case 'E':
		abs_time = str_expr_to_ull( opt_val, NULL, 0 );
		break;

	    case 'l':
		abs_time = lday_to_etime( str_expr_to_ul( opt_val, NULL, 0 ) );
		break;

	    case 'x':
		abs_time = xday_to_etime( str_expr_to_ul( opt_val, NULL, 0 ) );
		break;

	    case 'J':
		abs_time = jd_to_etime( str_expr_to_d( opt_val, NULL ) );
		break;

	    case 's':
                abs_time = time_to_etime( str_expr_to_ul( opt_val, NULL, 0 ) );
		break;

	    case 'm':
		abs_time = timems_to_etime( str_expr_to_ull( opt_val, NULL, 0 ) );
		break;

	    case 'u':
                abs_time = timeus_to_etime( str_expr_to_ull( opt_val, NULL, 0 ) );
		break;

	    case 'n':
                abs_time = timens_to_etime( str_expr_to_ull( opt_val, NULL, 0 ) );
		break;

	    default:
		fprintf( stderr, "%s: bad option at [%u]: \"%s\"\n",
			 pgm_name, arg_num, argv[arg_num] );
		return 1;
	    }
	    continue;
	}

	//-------------------------
	// Check for relative time.
	//-------------------------
	if ( opt_rel ) {
	    etime_t	rel_time	;

	    if ( abs_num_1 == 0 ) {
#ifdef PEDANTIC
		fprintf( stderr, "%s: an absolute date/time is needed before a relative time\n",
			 pgm_name );
		return 1;
#else
		abs_time = current_etime();
		abs_num_1 = arg_num;
#endif
	    }

	    rel_time = 0;
	    if ( convert_relative_time( opt_rel, & rel_time ) < 0 ) {
		fprintf( stderr, "%s: bad relative time value at [%u]: \"%s\"\n",
			 pgm_name, arg_num, argv[arg_num] );
		return 1;
	    }
	    if ( opt_code == '-' ) {
		abs_time -= rel_time;
	    } else {
		abs_time += rel_time;
	    }
	    continue;
	}

	//----------------------------------------
	// Nothing recognized so report the error.
	//----------------------------------------
	fprintf( stderr, "%s: bad option at [%u]: \"%s\"\n",
		 pgm_name, arg_num, argv[arg_num] );
	return 1;
    }

    //-------------------------------------------------
    // If no absolute time was given, this is an error.
    //-------------------------------------------------
    if ( abs_num_1 == 0 ) {
#ifdef PEDANTIC
	fprintf( stderr, "%s: no absolute time given\n", pgm_name );
	return 1;
#else
	abs_time = current_etime();
#endif
    }

    //--------------------------------------
    // Otherwise, output the time formatted.
    //--------------------------------------
    etime_to_fprintf( stdout, format, abs_time );
    fputc( '\n', stdout );
    fflush( stdout );

    return 0;

 help:
    if ( time( NULL ) % 1 == 0 ) {
	fprintf( stderr, "Sometimes I just feel so helpless.\n" );
    } else {
	fprintf( stderr, "Help!  I've fallen and can't get up.\n" );
    }
    return 1;
}

