/** @file
 * @brief date filtering using value ranges
 */
/* Copyright (C) 2006,2015,2021 Olly Betts
 *
 * 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, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "datevalue.h"

#include <cstdlib>
#include <cstring>
#include <ctime>

#include <xapian.h>

#include "parseint.h"
#include "timegm.h"
#include "values.h"

using namespace std;

class DateRangeLimit {
    struct tm tm;

    static int DIGIT(char ch) { return ch - '0'; }

    static int DIGIT2(const char *p) {
	return DIGIT(p[0]) * 10 + DIGIT(p[1]);
    }

    static int DIGIT4(const char *p) {
	return DIGIT2(p) * 100 + DIGIT2(p + 2);
    }

    int is_leap_year() const {
	int y = tm.tm_year;
	return (y % 4 == 0 && (y % 100 != 0 || y % 400 == 100));
    }

    int month_length() const {
	static const int usual_month_length[12] = {
	    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
	};
	if (tm.tm_mon == 1 && is_leap_year()) return 29;
	return usual_month_length[tm.tm_mon];
    }

  public:
    DateRangeLimit() { tm.tm_sec = -1; }

    DateRangeLimit(const string & str, bool start);

    explicit DateRangeLimit(time_t secs) {
#ifdef HAVE_GMTIME_R
	if (gmtime_r(&secs, &tm) == NULL) {
	    tm.tm_sec = -1;
	}
#else
	// Not thread-safe, but that isn't important for our uses here.
	struct tm * r = gmtime(&secs);
	if (r) {
	    tm = *r;
	} else {
	    tm.tm_sec = -1;
	}
#endif
    }

    bool is_set() const { return tm.tm_sec >= 0; }

    DateRangeLimit operator-(int span) {
	if (!is_set()) return *this;
	return DateRangeLimit(timegm(&tm) - span);
    }

    DateRangeLimit operator+(int span) {
	if (!is_set()) return *this;
	return DateRangeLimit(timegm(&tm) + span);
    }

    string format(bool start) const;

    string bin4(bool start) {
	if (!is_set()) {
	    return start ? string() : string(4, '\xff');
	}
	time_t s = timegm(&tm);
	if (s <= 0) {
	    // This encoding can't represent dates before the epoch, so treat
	    // the range as having an open start.
	    return string();
	}
	if constexpr(sizeof(time_t) > 4) {
	    if (s >= 0xffffffff) {
		// This encoding can't represent dates after 0xffffffff, so
		// clamp the range end to that and Xapian's matcher will see
		// the range end is >= the slot upper bound and optimise this
		// to an open upper range end.
		return string(4, '\xff');
	    }
	}
	return int_to_binary_string(uint32_t(s));
    }
};

DateRangeLimit::DateRangeLimit(const string & str, bool start)
{
    if (str.empty()) {
	tm.tm_sec = -1;
	return;
    }

    memset(&tm, 0, sizeof(struct tm));
    if (start) {
	tm.tm_mday = 1;
    } else {
	tm.tm_mon = 11;
	tm.tm_hour = 23;
	tm.tm_min = 59;
	tm.tm_sec = 59;
    }
    const char * p = str.data();
    tm.tm_year = DIGIT4(p) - 1900;

    if (str.size() >= 6) {
	tm.tm_mon = DIGIT2(p + 4) - 1;
	if (str.size() >= 8) {
	    tm.tm_mday = DIGIT2(p + 6);
	    if (str.size() >= 10) {
		tm.tm_hour = DIGIT2(p + 8);
		if (str.size() >= 12) {
		    tm.tm_min = DIGIT2(p + 10);
		    if (str.size() >= 14) {
			tm.tm_sec = DIGIT2(p + 12);
		    }
		}
	    }
	    return;
	}
    }

    if (!start) tm.tm_mday = month_length();
}

string
DateRangeLimit::format(bool start) const
{
    if (!is_set()) {
	return start ? string() : string(1, '~');
    }

    char fmt[] = "%Y%m%d%H%M%S";
    size_t fmt_len = sizeof(fmt) - 1;

    if (start) {
	if (tm.tm_sec == 0) {
	    if (tm.tm_min == 0) {
		if (tm.tm_hour == 0) {
		    if (tm.tm_mday <= 1) {
			if (tm.tm_mon == 0) {
			    fmt_len = 2;
			} else {
			    fmt_len = 4;
			}
		    } else {
			fmt_len = 6;
		    }
		} else {
		    fmt_len = 8;
		}
	    } else {
		fmt_len = 10;
	    }
	}
    } else {
	// If there's a leap second right after a range end, it'll just get
	// treated as being in the range.  This doesn't seem like a big
	// issue, and if we worry about getting that case right, we can never
	// contract a range unless it ends on second 60, or we know when all
	// the leap seconds are.
	if (tm.tm_sec >= 59) {
	    if (tm.tm_min >= 59) {
		if (tm.tm_hour >= 23) {
		    if (tm.tm_mday >= month_length()) {
			if (tm.tm_mon >= 11) {
			    fmt_len = 2;
			} else {
			    fmt_len = 4;
			}
		    } else {
			fmt_len = 6;
		    }
		} else {
		    fmt_len = 8;
		}
	    } else {
		fmt_len = 10;
	    }
	}
    }
    fmt[fmt_len] = '\0';
    char buf[15];
    size_t len = strftime(buf, sizeof(buf), fmt, &tm);
    if (start) {
	while (len && buf[len - 1] == '0') --len;
    } else {
	while (len && buf[len - 1] == '9') --len;
	if (len < 14) {
	    // Append a character that will sort after any valid extra
	    // precision.
	    buf[len++] = '~';
	}
    }
    return string(buf, len);
}

Xapian::Query
date_value_range(bool as_time_t,
		 Xapian::valueno slot,
		 const std::string & date_start,
		 const std::string & date_end,
		 const std::string & date_span)
{
    DateRangeLimit start(date_start, true);
    DateRangeLimit end(date_end, false);

    if (!date_span.empty()) {
	unsigned int days;
	if (!parse_unsigned(date_span.c_str(), days)) {
	    throw "Datespan value must be >= 0";
	}
	time_t span = days * (24 * 60 * 60) - 1;
	if (end.is_set()) {
	    // If START, END and SPAN are all set, we (somewhat arbitrarily)
	    // ignore START.
	    start = end - span;
	} else if (start.is_set()) {
	    end = start + span;
	} else {
	    // Only SPAN is set, so go back from now.
	    time_t now = time(NULL);
	    end = DateRangeLimit(now);
	    start = end - span;
	}
    }

    if (as_time_t) {
	return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, slot,
			     start.bin4(true), end.bin4(false));
    }

    return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, slot,
			 start.format(true), end.format(false));
}
