/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2017-2018 Danny Robson <danny@nerdcruft.net>
 */

#include "time/parse.hpp"

#include "debug.hpp"
#include "posix/except.hpp"

#include <stdexcept>
#include <iostream>

// We generate some really old style C code via ragel here, so we have to
// disable some noisy warnings (doubly so given -Werror)
#pragma GCC diagnostic ignored "-Wold-style-cast"


///////////////////////////////////////////////////////////////////////////////
%%{
    # based off rfc3339 rather than iso8601 because the former is public

    machine iso8601;

    date_fullyear = digit{4};
    date_month    = digit{2};
    date_mday     = digit{2};

    time_hour     = digit{2};
    time_minute   = digit{2};
    time_second   = digit{2};

    time_secfrac  = '.' digit{1,} ${ frac *= 10; frac += fc - '0'; };
    time_numoffset = ('+' %{dir=1;} | '-' %{dir=-1;})
                         time_hour   ${ offset.tm_hour *= 10; offset.tm_hour += fc - '0'; }
                     ':' time_minute ${ offset.tm_min  *= 10; offset.tm_min  += fc - '0'; };
    time_offset    = 'Z' | time_numoffset;

    partial_time =     time_hour    ${ parts.tm_hour *= 10; parts.tm_hour += fc - '0'; }
                   ':' time_minute  ${ parts.tm_min  *= 10; parts.tm_min  += fc - '0'; }
                   ':' time_second  ${ parts.tm_sec  *= 10; parts.tm_sec  += fc - '0'; }
                       time_secfrac?;

    full_date =     date_fullyear ${ parts.tm_year *= 10; parts.tm_year += fc - '0'; }
                '-' date_month    ${ parts.tm_mon  *= 10; parts.tm_mon  += fc - '0'; }
                '-' date_mday     ${ parts.tm_mday *= 10; parts.tm_mday += fc - '0'; };

    full_time = partial_time time_offset;

    date_time := (
        full_date 'T' full_time
    )
    >{ success = false; }
    %{ success = true;  };

    write data;
}%%


///////////////////////////////////////////////////////////////////////////////
template <>
bool
cruft::debug::validator<tm>::is_valid (const tm &val) noexcept
{
    // we don't test tm_year anywhere here because there isn't a valid range
    // for years, only that they are expressed as offsets from 1900;
    return val.tm_sec  >= 0 && val.tm_sec  <=  60 &&
           val.tm_min  >= 0 && val.tm_min  <   60 &&
           val.tm_hour >= 0 && val.tm_hour <   24 &&
           val.tm_mday >  0 && val.tm_mday <=  31 &&
           val.tm_mon  >= 0 && val.tm_mon  <   12 &&
           val.tm_wday >= 0 && val.tm_wday <    7 &&
           val.tm_yday >= 0 && val.tm_yday <= 365;
}


//-----------------------------------------------------------------------------
std::ostream&
operator<< (std::ostream &os, const tm&)
{
    return os << "{}";
}


//-----------------------------------------------------------------------------
std::chrono::seconds
to_epoch (const tm &t)
{
    // TODO: it's assumed the user isn't passing in oddities like 36 months or
    // similar. in the future we can account for this
    CHECK_SANITY (t);

    constexpr int
    cumulative_days [12] = {
        0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
    };

    constexpr int epoch_year = 1970;
    const int year = 1900 + t.tm_year;

    // find the number of days since 1970. careful of leap years.
    time_t secs;
    secs  = (year - epoch_year) * 365 + cumulative_days[t.tm_mon % 12];
    secs += (year - epoch_year + epoch_year %   4) /   4;
    secs -= (year - epoch_year + epoch_year % 100) / 100;
    secs += (year - epoch_year + epoch_year % 400) / 400;

    const bool is_leap_year = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    if (is_leap_year && t.tm_mon < 2)
        secs--;

    secs += t.tm_mday - 1;

    // hours
    secs *= 24;
    secs += t.tm_hour;

    // minutes
    secs *= 60;
    secs += t.tm_min;

    // seconds
    secs *= 60;
    secs += t.tm_sec;

    if (t.tm_isdst)
        secs -= 60 * 60;

    return std::chrono::seconds {secs};
}



//-----------------------------------------------------------------------------
std::chrono::nanoseconds
cruft::time::iso8601::parse (cruft::view<const char*> str)
{
    int cs;
    const char *p   = std::begin (str);
    const char *pe  = std::end    (str);
    const char *eof = pe;

    bool success = false;

    int dir = 0;
    int64_t frac = 0;
    struct tm parts, offset;
    memset (&parts, 0, sizeof (parts));
    memset (&offset, 0, sizeof (offset));

    %%write init;
    %%write exec;

    if (!success)
        throw std::invalid_argument ("invalid date string");

    parts.tm_year -= 1900;
    parts.tm_mon  -= 1;

    // compute the timezone offset
    std::chrono::seconds diff {
        dir * (offset.tm_hour * 60 * 60 + offset.tm_min * 60)
    };

    // fractional part
    auto nano_digits = cruft::digits10 (std::nano::den-1);
    auto frac_digits = cruft::digits10 (frac);
    auto shift = cruft::pow (10, unsigned(nano_digits - frac_digits));

    // sum the time_t, timezone offset, and fractional components
    return to_epoch (parts) - diff + std::chrono::nanoseconds (frac * shift);
}