/***************************** LICENSE START ***********************************

 Copyright 2012 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include <time.h>
#include "MvApplication.h"
#include "macro.h"


const char* Date::stringFormat = "yyyy-mm-dd HH:MM:SS";
const char* Date::numberFormat = "yyyymmdd";

const double cSecEpsilon = 0.000001157;  //-- 0.1 sec

const char* _mvmonth[12][2] = {
    {
        "Jan",
        "January",
    },
    {
        "Feb",
        "February",
    },
    {
        "Mar",
        "March",
    },
    {
        "Apr",
        "April",
    },
    {
        "May",
        "May",
    },
    {
        "Jun",
        "June",
    },
    {
        "Jul",
        "July",
    },
    {
        "Aug",
        "August",
    },
    {
        "Sep",
        "September",
    },
    {
        "Oct",
        "October",
    },
    {
        "Nov",
        "November",
    },
    {
        "Dec",
        "December",
    },
};

const char* _mvday[7][2] = {
    {
        "Mon",
        "Monday",
    },
    {
        "Tue",
        "Tuesday",
    },
    {
        "Wed",
        "Wednesday",
    },
    {
        "Thu",
        "Thursday",
    },
    {
        "Fri",
        "Friday",
    },
    {
        "Sat",
        "Saturday",
    },
    {
        "Sun",
        "Sunday",
    },
};

//============================================================================

void CDate::ToNumber(double& x)
{
    char buf[1024];
    date.Format(Date::NumberFormat(), buf);
    x = atof(buf);
}

void CDate::ToString(const char*& x)
{
    static char buf[20];
    date.Format(Date::StringFormat(), buf);
    x = buf;
}

void CDate::Dump2()
{
    std::cout << "date(" << date._julian() << "," << date._second() << ")";
}

//============================================================================

int Date::Julian()
{
    Date jan1(Year() * 10000 + 100 + 1);
    return (int)(*this - jan1 + 1);
}

static request* pref()
{
    static request* p = nullptr;
    static MvRequest r;  // declare locally to avoid the destructor removing it

    if (p == nullptr) {
        r = MvApplication::getPreferences();
        p = r;
    }
    return p;
}

const char* Date::MonthName(int n, boolean full)
{
    request* p = pref();
    const char* s = get_value(p,
                              full ? "MONTH_LONG_NAMES" : "MONTH_SHORT_NAMES", n - 1);
    return s ? s : _mvmonth[n - 1][int(full)];
}

const char* Date::DayName(int n, boolean full)
{
    request* p = pref();
    const char* s = get_value(p, full ? "DAY_LONG_NAMES" : "DAY_SHORT_NAMES", n - 1);
    return s ? s : _mvday[n - 1][int(full)];
}

const char* Date::StringFormat(void)
{
    request* p = pref();
    const char* s = no_quotes(get_value(p, "STRING_DATE_FORMAT", 0));
    return s ? s : stringFormat;
}

const char* Date::NumberFormat(void)
{
    request* p = pref();
    const char* s = no_quotes(get_value(p, "NUMBER_DATE_FORMAT", 0));
    return s ? s : numberFormat;
}

void Date::Print(void)
{
    char buf[1024];
    Format(StringFormat(), buf);
    std::cout << buf;
}


Date::Date(double n)
{
    julian = (long)n;
    double s = n - (double)julian;

    if (s < 0.0) {  //-- 'n' is a negative decimal value; decimal part thus
        //-- protrudes into the previous julian day; also we need
        //-- to "reverse" decimal part, e.g.: -1.25 => -2 + 0.75
        julian--;
        second = (long)((1.0 - (-s) + cSecEpsilon) * 86400.0);
    }
    else {  //-- 'n' is an integer, or a positive decimal value
        second = (long)((n - julian + cSecEpsilon) * 86400.0);
    }

    // special case to capture: if the supplied number is already a
    // Julian date, then we just store it. This should be safe, since
    // the numbers involved are not valid as YYMMDD or YYYYMMDD, and
    // before the introduction of this test, any positive number not
    // in YYYYMMDD format would just produce a garbage date.
    if (julian >= 1721426 && julian <= 3182030)  // 0001-01-01 to 4000-01-01
    {
        // just keep the number, as it's already in Julian format!
    }
    else {
        julian = mars_date_to_julian(julian);

        while (second < 0) {
            second += 86400;
            julian++;
        }
        while (second > 86399) {
            second -= 86400;
            julian--;
        }
    }
}

Date::Date(long d, long t)
{
    julian = mars_date_to_julian(d);

    second = ((t / 100) * 60 + (t % 100)) * 60;
    while (second < 0) {
        second += 86400;
        julian++;
    }
    while (second > 86399) {
        second -= 86400;
        julian--;
    }
}

Date::Date(const char* s)
{
    boolean dum;
    if (!parsedate(s, &julian, &second, &dum)) {
        julian = mars_date_to_julian(atol(s));
        second = 0;
    }
}


Date Date::operator+(double& n)
{
    Date date;
    double secEps = n < 0.0 ? -cSecEpsilon : cSecEpsilon;

    long j = (long)n;
    long s = (long)(((n - (double)j) + secEps) * 86400.0);

    date.julian = julian + j;
    date.second = second + s;

    while (date.second < 0) {
        date.second += 86400;
        date.julian--;
    }
    while (date.second > 86399) {
        date.second -= 86400;
        date.julian++;
    }

    return date;
}

Date Date::operator-(double& n)
{
    double m = -n;
    return operator+(m);
}

double Date::operator-(Date& d)
{
    return (double)(julian - d.julian) + (double)(second - d.second) / 86400.0;
}

//=============================================================================

static int collect(const char*& f, char* buf, int& i, char& c)
{
    int n;
    c = 0;
    while (*f)
        switch (*f) {
            case 'y':
            case 'm':
            case 'd':
            case 'D':
            case 'H':
            case 'M':
            case 'S':
                c = *f;
                n = 1;
                while (*++f == c)
                    n++;
                return n;
                // break;

            default:
                buf[i++] = *f++;
                break;
        }
    return -1;
}

static void copy(char* buf, int& i, int n, char c)
{
    for (int j = 0; j < n; j++)
        buf[i++] = c;
}

static void copy(char* buf, int& i, const char* f, int n)
{
    char tmp[20];
    char* p = tmp;

    sprintf(tmp, f, n);

    while (*p)
        buf[i++] = *p++;
}

static void copy(char* buf, int& i, const char* p)
{
    while (*p)
        buf[i++] = *p++;
}

void Date::Format(const char* f, char* buf)
{
    int i = 0;
    char c = 0;

    for (;;) {
        int n = collect(f, buf, i, c);
        switch (c) {
            case 'y':
                switch (n) {
                    case 2:
                        copy(buf, i, "%02d", Year() % 100);
                        break;

                    case 4:
                        copy(buf, i, "%04d", Year());
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'm':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Month());
                        break;

                    case 2:
                        copy(buf, i, "%02d", Month());
                        break;

                    case 3:
                        copy(buf, i, MonthName(Month(), false));
                        break;

                    case 4:
                        copy(buf, i, MonthName(Month(), true));
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'd':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Day());
                        break;

                    case 2:
                        copy(buf, i, "%02d", Day());
                        break;

                    case 3:
                        copy(buf, i, DayName(DayOfWeek(), false));
                        break;

                    case 4:
                        copy(buf, i, DayName(DayOfWeek(), true));
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'D':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Julian());
                        break;

                    case 3:
                        copy(buf, i, "%03d", Julian());
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'H':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Hour());
                        break;

                    case 2:
                        copy(buf, i, "%02d", Hour());
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'M':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Minute());
                        break;

                    case 2:
                        copy(buf, i, "%02d", Minute());
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            case 'S':
                switch (n) {
                    case 1:
                        copy(buf, i, "%d", Second());
                        break;

                    case 2:
                        copy(buf, i, "%02d", Second());
                        break;

                    default:
                        copy(buf, i, n, c);
                        break;
                }
                break;

            default:
                break;
        }

        if (n < 0)
            break;
    }

    buf[i] = 0;
}

//=============================================================================

class TimeDateFunction : public Function
{
    char type;

public:
    TimeDateFunction(const char* n, char t) :
        Function(n, 1, tnumber),
        type(t)
    {
        info = "Converts a number to a value suitable for date computations";
    }

    virtual Value Execute(int arity, Value* arg);
};

Value TimeDateFunction::Execute(int, Value* arg)
{
    double d;
    arg[0].GetValue(d);

    switch (type) {
        case 'd':
            d *= 86400;
            break;
        case 'h':
            d *= 3600;
            break;
        case 'm':
            d *= 60;
            break;
        case 's':
            break;
    }
    return Value(d / 86400);
}
//=============================================================================

class DateTimeFunction : public Function
{
    char type;

public:
    DateTimeFunction(const char* n, char t) :
        Function(n, 1, tdate),
        type(t)
    {
        info = "Extract a date component";
    }

    virtual Value Execute(int arity, Value* arg);
};

Value DateTimeFunction::Execute(int, Value* arg)
{
    Date d;
    arg[0].GetValue(d);
    int n = 0;

    switch (type) {
        case 'y':
            n = d.Year();
            break;
        case 'M':
            n = d.Month();
            break;
        case 'd':
            n = d.Day();
            break;
        case 'h':
            n = d.Hour();
            break;
        case 'm':
            n = d.Minute();
            break;
        case 's':
            n = d.Second();
            break;
        case 'w':
            n = d.DayOfWeek();
            break;
        case 'a':
            n = d.YyMmDd();
            break;
        case 'b':
            n = d.YyyyMmDd();
            break;
        case 'j':
            n = d.Julian();
            break;
        case 'J':
            n = int(d._julian());
            break;
        case '1':
            n = d.Hour() * 100 + d.Minute();
            break;
        case '2':
            n = d.Hour() * 10000 + d.Minute() * 100 + d.Second();
            break;
    }
    return Value(n);
}

//=============================================================================

class DateStringFunction : public Function
{
    boolean do_string;

public:
    DateStringFunction(const char* n, boolean s) :
        Function(n),
        do_string(s) {}
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

Value DateStringFunction::Execute(int arity, Value* arg)
{
    Date d;
    const char* f = do_string ? Date::StringFormat() : Date::NumberFormat();
    char buf[1024];

    arg[0].GetValue(d);

    if (arity == 2)
        arg[1].GetValue(f);

    d.Format(f, buf);

    return do_string ? Value(buf) : Value(atof(buf));
}

int DateStringFunction::ValidArguments(int arity, Value* arg)
{
    if (arity != 1 && arity != 2)
        return false;
    if (arg[0].GetType() != tdate)
        return false;
    if (arity == 2 && arg[1].GetType() != tstring)
        return false;
    return true;
}

//=============================================================================

class DateFunction : public Function
{
public:
    DateFunction(const char* n) :
        Function(n)
    {
        info = "Creates a date from a number or a string";
    }
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

class DateNowFunction : public Function
{
public:
    DateNowFunction(const char* n) :
        Function(n)
    {
        info = "Returns the current date and time";
    }
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

class DateAddFunction : public Function
{
public:
    DateAddFunction(const char* n) :
        Function(n, 2, tdate, tnumber)
    {
        info = "Adds a number to a date";
    }
    virtual Value Execute(int arity, Value* arg);
};

class DateSubFunction : public Function
{
public:
    DateSubFunction(const char* n) :
        Function(n, 2, tdate, tnumber | tdate) {}
    virtual Value Execute(int arity, Value* arg);
};

class DateCmpFunction : public Function
{
    using cproc = int (*)(Date*, Date*);
    cproc F_;

public:
    DateCmpFunction(const char* n, cproc c) :
        Function(n, 2, tdate, tdate) { F_ = c; }
    virtual Value Execute(int arity, Value* arg);
};

//=============================================================================

int DateFunction::ValidArguments(int arity, Value* arg)
{
    int i;
    if (arity != 1 && arity != 3 && arity != 6)
        return 0;

    switch (arg[0].GetType()) {
        case tnumber:
            for (i = 1; i < arity; i++)
                if (arg[i].GetType() != tnumber)
                    return 0;
            return 1;
            /* break; */

        case tstring:
            return arity == 1;
            /* break; */

        default:
            return 0;
            /* break; */
    }
}

Value DateFunction::Execute(int arity, Value* arg)
{
    if (arg[0].GetType() == tnumber) {
        double date = 0.;
        double yy, mm, dd, HH, MM, SS;

        switch (arity) {
            case 1:
                arg[0].GetValue(date);
                break;

            case 3:
                arg[0].GetValue(yy);
                arg[1].GetValue(mm);
                arg[2].GetValue(dd);
                date = yy * 10000 + mm * 100 + dd;
                break;

            case 6:
                arg[0].GetValue(yy);
                arg[1].GetValue(mm);
                arg[2].GetValue(dd);
                arg[3].GetValue(HH);
                arg[4].GetValue(MM);
                arg[5].GetValue(SS);
                date = yy * 10000 + mm * 100 + dd +
                       HH / 24.0 + MM / 24.0 / 60.0 + SS / 24.0 / 60.0 / 60.0;
                break;

            default:
                break;
        }

        Date x(date);
        return Value(x);
    }
    else {
        const char* s;
        arg[0].GetValue(s);
        Date x(s);
        return Value(x);
    }
}

int DateNowFunction::ValidArguments(int arity, Value* /*arg*/)
{
    if (arity == 0)
        return 1;
    else
        return 0;
}

Value DateNowFunction::Execute(int, Value*)
{
    time_t tTime;
    struct tm* xTime;

    (void)time(&tTime);
    xTime = localtime(&tTime);

    double myDate = (xTime->tm_year + 1900) * 10000 + (xTime->tm_mon + 1) * 100 + xTime->tm_mday;
    double mySecs = (xTime->tm_hour) * 3600 + (xTime->tm_min) * 60 + xTime->tm_sec;

    Date myD(myDate + mySecs / 86400.0);

    return Value(myD);
}

Value DateAddFunction::Execute(int, Value* arg)
{
    Date d;
    double n;

    arg[0].GetValue(d);
    arg[1].GetValue(n);

    d = d + n;

    return Value(d);
}


Value DateSubFunction::Execute(int, Value* arg)
{
    Date d;
    Date p;
    double n;

    arg[0].GetValue(d);

    if (arg[1].GetType() == tnumber) {
        arg[1].GetValue(n);
        p = d - n;
        return Value(p);
    }
    else {
        arg[1].GetValue(p);
        n = d - p;
        return Value(n);
    }
}

//=============================================================================

static int d_gt(Date* a, Date* b)
{
    return *a > *b;
}
static int d_lt(Date* a, Date* b)
{
    return *a < *b;
}
static int d_eq(Date* a, Date* b)
{
    return *a == *b;
}
static int d_ne(Date* a, Date* b)
{
    return *a != *b;
}
static int d_ge(Date* a, Date* b)
{
    return *a >= *b;
}
static int d_le(Date* a, Date* b)
{
    return *a <= *b;
}


Value DateCmpFunction::Execute(int, Value* arg)
{
    Date d1;
    Date d2;

    arg[0].GetValue(d1);
    arg[1].GetValue(d2);

    return Value(F_(&d1, &d2));
}


//=============================================================================

class DateAddMonthsFunction : public Function
{
public:
    DateAddMonthsFunction(const char* n) :
        Function(n, 2, tdate, tnumber)
    {
        info = "Adds months to a date";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value DateAddMonthsFunction::Execute(int, Value* arg)
{
    Date di;
    arg[0].GetValue(di);
    int y = di.Year();
    int m = di.Month();
    int d = di.Day();

    int n;
    arg[1].GetValue(n);

    m = m + n;

    if (n < 0) {
        while (m < 0) {
            --y;
            m = m + 12;
        }
    }
    else {
        while (m > 12) {
            ++y;
            m = m - 12;
        }
    }

    return Value(Date(y * 10000.0 + m * 100.0 + d));
}

//=============================================================================

static void install(Context* c)
{
    c->AddFunction(new TimeDateFunction("hour", 'h'));
    c->AddFunction(new TimeDateFunction("minute", 'm'));
    c->AddFunction(new TimeDateFunction("second", 's'));
    c->AddFunction(new TimeDateFunction("day", 'd'));

    c->AddFunction(new DateTimeFunction("hour", 'h'));
    c->AddFunction(new DateTimeFunction("minute", 'm'));
    c->AddFunction(new DateTimeFunction("second", 's'));
    c->AddFunction(new DateTimeFunction("day", 'd'));
    c->AddFunction(new DateTimeFunction("month", 'M'));
    c->AddFunction(new DateTimeFunction("year", 'y'));
    c->AddFunction(new DateTimeFunction("dow", 'w'));
    c->AddFunction(new DateTimeFunction("yymmdd", 'a'));
    c->AddFunction(new DateTimeFunction("yyyymmdd", 'b'));
    c->AddFunction(new DateTimeFunction("hhmm", '1'));
    c->AddFunction(new DateTimeFunction("hhmmss", '2'));
    c->AddFunction(new DateTimeFunction("julday", 'j'));
    c->AddFunction(new DateTimeFunction("juldate", 'J'));

    c->AddFunction(new DateFunction("date"));
    c->AddFunction(new DateNowFunction("now"));
    c->AddFunction(new DateSubFunction("-"));
    c->AddFunction(new DateAddFunction("+"));
    c->AddFunction(new DateAddMonthsFunction("addmonths"));
    c->AddFunction(new DateStringFunction("string", true));
    c->AddFunction(new DateStringFunction("number", false));

    c->AddFunction(new DateCmpFunction(">", d_gt));
    c->AddFunction(new DateCmpFunction("<", d_lt));
    c->AddFunction(new DateCmpFunction(">=", d_ge));
    c->AddFunction(new DateCmpFunction("<=", d_le));
    c->AddFunction(new DateCmpFunction("=", d_eq));
    c->AddFunction(new DateCmpFunction("<>", d_ne));
}

static Linkage linkage(install);
