#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/time.h>
#include <sys/stat.h>
#include "lib/mlr_globals.h"
#include "lib/mlr_arch.h"
#include "lib/mlrutil.h"
#include "lib/mlrdatetime.h"
#include "lib/string_builder.h"

// NZ since this isn't counting the null terminator.
#define NZBUFLEN 63

// ----------------------------------------------------------------
// seconds since the epoch
double get_systime() {
	struct timeval tv = { .tv_sec = 0, .tv_usec = 0 };
	(void)gettimeofday(&tv, NULL);
	return (double)tv.tv_sec + (double)tv.tv_usec * 1e-6;
}

// ----------------------------------------------------------------
// The essential idea is that we use the library function gmtime to get a struct tm, then strftime
// to produce a formatted string. The only complication is that we support "%1S" through "%9S" for
// formatting the seconds with a desired number of decimal places.

char* mlr_alloc_time_string_from_seconds(double seconds_since_the_epoch, char* format_string,
	timezone_handling_t timezone_handling)
{

	// 1. Split out the integer seconds since the epoch, which the stdlib can handle, and
	//    the fractional part, which it cannot.
	time_t iseconds = (time_t) seconds_since_the_epoch;
	double fracsec = seconds_since_the_epoch - iseconds;

	struct tm tm;
	switch(timezone_handling) {
	case TIMEZONE_HANDLING_GMT:
		tm = *gmtime(&iseconds); // No gmtime_r on Windows so just use gmtime.
		break;
	case TIMEZONE_HANDLING_LOCAL:
		tm = *localtime(&iseconds); // No gmtime_r on Windows so just use gmtime.
		break;
	default:
		fprintf(stderr, "%s: internal coding error detected in file %s at line %d.\n",
			MLR_GLOBALS.bargv0, __FILE__, __LINE__);
		exit(1);
		break;
	}

	// 2. See if "%nS" (for n in 1..9) is a substring of the format string.
	char* middle_nS_format = NULL;
	char* right_subformat = NULL;
	int n = 0; // Save off n for round-up handling below.
	for (char* p = format_string; *p; p++) {
		// We can't use strstr since we're searching for a pattern, and regexes are overkill.
		// Here we rely on left-to-right evaluation of the boolean expressions, with non-evaluation
		// of a subexpression if a subexpression to its left is false (this keeps us from reading
		// past end of input string).
		if (p[0] == '%' && p[1] >= '1' && p[1] <= '9' && p[2] == 'S') {
			middle_nS_format = p;
			right_subformat = &p[3];
			n = p[1] - '0';
			break;
		}
	}

	// 3. If "%nS" (for n in 1..9) is not a substring of the format string, just use strftime.
	if (middle_nS_format == NULL) {
		char* output_string = mlr_malloc_or_die(NZBUFLEN+1);
		int written_length = strftime(output_string, NZBUFLEN, format_string, &tm);
		if (written_length > NZBUFLEN || written_length == 0) {
			fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
				MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
			exit(1);
		}

		return output_string;
	}

	// Now we know "%nS" (for n in 1..9) is a substring of the format string.  Idea is to
	// copy the subformats to the left and right of the %nS part and format them both using
	// strftime, and format the middle part ourselves using sprintf.  Then concatenate all
	// those pieces.

	// 5. Find the left-of-%nS subformat, and format the input using that.
	int left_subformat_length = middle_nS_format - format_string;
	char* left_subformat = mlr_malloc_or_die(left_subformat_length + 1);
	memcpy(left_subformat, format_string, left_subformat_length);
	left_subformat[left_subformat_length] = 0;

	char left_formatted[NZBUFLEN+1];
	if (*left_subformat == 0) {
		// There's nothing to the left of %nS. strftime will error on empty format string, so we can
		// just map empty format to empty result ourselves.
		*left_formatted = 0;
	} else {
		int written_length = strftime(left_formatted, NZBUFLEN, left_subformat, &tm);
		if (written_length > NZBUFLEN || written_length == 0) {
			fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
				MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
			exit(1);
		}
	}
	free(left_subformat);

	// 6. There are two parts in the middle: the integer part which strftime can populate
	//    from the struct tm, using %S format, and the fractional-seconds part which we sprintf.
	//    First do the int part.
	char middle_int_formatted[NZBUFLEN+1];
	char* middle_int_format = "%S";
	int written_length = strftime(middle_int_formatted, NZBUFLEN, middle_int_format, &tm);
	if (written_length > NZBUFLEN || written_length == 0) {
		fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
			MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
		exit(1);
	}

	// 7. Do the fractional-seconds part. One key point is that sprintf always writes a leading zero,
	//    e.g. .123456 becomes "0.123456". We'll take off the leading zero later.
	char middle_sprintf_format[] = "%.xlf";
	char middle_fractional_formatted[16];
	// "%6S" maps to "%.6lf" and so on. We found the middle_nS_format by searching for "%nS" for
	// n in 1..9 so sprintf-format subscript 2 is the same as strftime format subscript 1.
	middle_sprintf_format[2] = middle_nS_format[1];
	sprintf(middle_fractional_formatted, middle_sprintf_format, fracsec);

	// 8. Format the right-of-%nS part, also using strftime.
	char right_formatted[NZBUFLEN];
	if (*right_subformat == 0) {
		// There's nothing to the right of %nS. strftime will error on empty format string, so we can
		// just map empty format to empty result ourselves.
		*right_formatted = 0;
	} else {
		int written_length = strftime(right_formatted, NZBUFLEN, right_subformat, &tm);
		if (written_length > NZBUFLEN || written_length == 0) {
			fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
				MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
			exit(1);
		}
	}

	// 9. Concatenate the output. For string_builder, the size argument is just an initial size;
	//    it can realloc beyond that initial estimate if necessary.
	string_builder_t* psb = sb_alloc(NZBUFLEN+1);
	sb_append_string(psb, left_formatted);
	sb_append_string(psb, middle_int_formatted);
	// When the input has fractional seconds like 0.999999 and the format is shorter than that,
	// e.g. "%3S", there can be round-up to 1.0 on the sprintf.
	if (middle_fractional_formatted[0] == '1') {
		sb_append_char(psb, '.');
		for (int i = 0; i < n; i++)
			sb_append_char(psb, '9');
	} else if (middle_fractional_formatted[0] == '0') {
		sb_append_string(psb, &middle_fractional_formatted[1]);
	} else {
		MLR_INTERNAL_CODING_ERROR();
	}
	sb_append_string(psb, right_formatted);
	char* output_string = sb_finish(psb);
	sb_free(psb);

	return output_string;
}

// ----------------------------------------------------------------
// Miller supports fractional seconds in the input string, but strptime doesn't. So we have
// to play some tricks, inspired in part by some ideas on StackOverflow. Special shout-out
// to @tinkerware on Github for the push in the right direction! :)

double mlr_seconds_from_time_string(char* time_string, char* format_string,
	timezone_handling_t timezone_handling)
{
	struct tm tm;

	// 1. Just try strptime on the input as-is and return quickly if it's OK.
	memset(&tm, 0, sizeof(tm));
	char* strptime_retval = mlr_arch_strptime(time_string, format_string, &tm);
	if (strptime_retval != NULL) {
		if (*strptime_retval != 0) { // Extraneous stuff in the input not matching the format
			fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
				MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
			exit(1);
		}

        // printf("TIME_STRING   %s\n", time_string);
        // printf("FORMAT_STRING %s\n", time_string);
        // printf("tm_year  =%d\n", tm.tm_year);
        // printf("tm_mon   =%d\n", tm.tm_mon);
        // printf("tm_mday  =%d\n", tm.tm_mday);
        // printf("tm_wday  =%d\n", tm.tm_wday);
        // printf("tm_yday  =%d\n", tm.tm_yday);
        // printf("tm_hour  =%d\n", tm.tm_hour);
        // printf("tm_min   =%d\n", tm.tm_min);
        // printf("tm_sec   =%d\n", tm.tm_sec);
        // printf("tm_isdst =%d\n", tm.tm_isdst);

		return (double)mlr_arch_timegmlocal(&tm, timezone_handling);
	}

	// 2. Now either there's floating-point seconds in the input, or something else is wrong.
	//    First look for "%S" in the format string, for two reasons: (a) if there isn't "%S"
	//    then something else is wrong; (b) if there is, we'll need that to split the format
	//    string.
	char* pS = strstr(format_string, "%S");
	if (pS == NULL) {
		// strptime failure couldn't have been because of floating-point-seconds stuff. No
		// reason to try any harder.
		fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
			MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
		exit(1);
	}

	// There's "%S" in the format string, and the input has fractional seconds matching that
	// and no other problems, or there's something else wrong.
	//
	// Running example as we work through the rest:
	// * Input is  "2017-04-09T00:51:09.123456 TZBLAHBLAH"
	// * Format is "%Y-%m-%dT%H:%M:%S TZBLAHBLAH"

	// 3. Copy the format up to the %S but with nothing else after. This is temporary to help us locate
	//    the fractional-seconds part of the input string.
	//    Example temporary format: "%Y-%m-%dT%H:%M:%S"

	int truncated_format_string_length = pS - format_string + 2; // 2 for the "%S"
	char* truncated_format_string = mlr_malloc_or_die(truncated_format_string_length + 1);
	memcpy(truncated_format_string, format_string, truncated_format_string_length);
	truncated_format_string[truncated_format_string_length] = 0;

	// 4. strptime using that truncated format and ignore the tm. Only look at the string return value.
	//    Example return value: ".123456 TZBLAHBLAH"
	strptime_retval = mlr_arch_strptime(time_string, truncated_format_string, &tm);
	if (strptime_retval == NULL) {
		fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
			MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
		exit(1);
	}
	free(truncated_format_string);

	// 5. strtod the return value to find the fractional-seconds part, and whatever's after.
	//    Example fractional-seconds part: ".123456"
	//    Example stuff after: " TZBLAHBLAH"
	char* stuff_after = NULL;
	double fractional_seconds;
	if (strptime_retval[0] == '.' && !isdigit(strptime_retval[1])) {
		// If they had just a decimal point with no digits after, e.g. "10." seconds rather than "10.0",
		// that's OK, but strtod won't parse that as double.
		fractional_seconds = 0.0;
		stuff_after = &strptime_retval[1];
	} else {
		fractional_seconds = strtod(strptime_retval, &stuff_after);
		if (stuff_after == strptime_retval) {
			// Non-parseable
			fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
				MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
			exit(1);
		}
	}

	// 6. Make a copy of the input string with the fractional seconds elided.
	//    Example: "2017-04-09T00:51:09 TZBLAHBLAH"
	char* elided_fraction_input = mlr_malloc_or_die(strlen(time_string) + 1);
	int input_length_up_to_fractional_seconds = strptime_retval - time_string;
	memcpy(elided_fraction_input, time_string, input_length_up_to_fractional_seconds);
	strcpy(&elided_fraction_input[input_length_up_to_fractional_seconds], stuff_after);

	// 7. strptime the elided-fraction input string using the original format string. (This is like
	//    the input never had fractional seconds in the first place.) Get the tm parsed from that.
	memset(&tm, 0, sizeof(tm));
	strptime_retval = mlr_arch_strptime(elided_fraction_input, format_string, &tm);
	if (strptime_retval == NULL) {
		fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
			MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
		exit(1);
	}
	if (*strptime_retval != 0) { // Extraneous stuff in the input not matching the format
		fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
			MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
		exit(1);
	}
	free(elided_fraction_input);

	// 8. Convert the tm to a time_t (seconds since the epoch) and then add the fractional seconds.
	return mlr_arch_timegmlocal(&tm, timezone_handling) + fractional_seconds;
}
