#include "base.h"

#include <util/string/cast.h>
#include <util/stream/output.h>
#include <util/stream/mem.h>
#include <util/system/compat.h>
#include <util/memory/tempbuf.h>
#include <util/generic/string.h>
#include <util/generic/yexception.h>

TString Strftime(const char* format, const struct tm* tm) {
    size_t size = Max<size_t>(strlen(format) * 2 + 1, 107);
    for (;;) {
        TTempBuf buf(size);
        int r = strftime(buf.Data(), buf.Size(), format, tm);
        if (r != 0) {
            return TString(buf.Data(), r);
        }
        size *= 2;
    }
}

template <>
TDuration FromStringImpl<TDuration, char>(const char* s, size_t len) {
    return TDuration::Parse(TStringBuf(s, len));
}

template <>
bool TryFromStringImpl<TDuration, char>(const char* s, size_t len, TDuration& result) {
    return TDuration::TryParse(TStringBuf(s, len), result);
}

namespace {
    template <size_t N>
    struct TPad {
        int I;
    };

    template <size_t N>
    inline TPad<N> Pad(int i) {
        return {i};
    }

    inline IOutputStream& operator<<(IOutputStream& o, const TPad<2>& p) {
        if (p.I < 10) {
            if (p.I >= 0) {
                o << '0';
            }
        }

        return o << p.I;
    }

    inline IOutputStream& operator<<(IOutputStream& o, const TPad<4>& p) {
        if (p.I < 1000) {
            if (p.I >= 0) {
                if (p.I < 10) {
                    o << '0' << '0' << '0';
                } else if (p.I < 100) {
                    o << '0' << '0';
                } else {
                    o << '0';
                }
            }
        }

        return o << p.I;
    }

    inline IOutputStream& operator<<(IOutputStream& o, const TPad<6>& p) {
        if (p.I < 100000) {
            if (p.I >= 0) {
                if (p.I < 10) {
                    o << '0' << '0' << '0' << '0' << '0';
                } else if (p.I < 100) {
                    o << '0' << '0' << '0' << '0';
                } else if (p.I < 1000) {
                    o << '0' << '0' << '0';
                } else if (p.I < 10000) {
                    o << '0' << '0';
                } else {
                    o << '0';
                }
            }
        }

        return o << p.I;
    }

    void WriteMicroSecondsToStream(IOutputStream& os, ui32 microSeconds) {
        os << '.' << Pad<6>(microSeconds);
    }

    void WriteTmToStream(IOutputStream& os, const struct tm& theTm) {
        os << Pad<4>(theTm.tm_year + 1900) << '-' << Pad<2>(theTm.tm_mon + 1) << '-' << Pad<2>(theTm.tm_mday) << 'T'
           << Pad<2>(theTm.tm_hour) << ':' << Pad<2>(theTm.tm_min) << ':' << Pad<2>(theTm.tm_sec);
    }

    template <bool PrintUpToSeconds, bool iso>
    void WritePrintableLocalTimeToStream(IOutputStream& os, const ::NPrivate::TPrintableLocalTime<PrintUpToSeconds, iso>& timeToPrint) {
        const TInstant& momentToPrint = timeToPrint.MomentToPrint;
        struct tm localTime;
        momentToPrint.LocalTime(&localTime);
        WriteTmToStream(os, localTime);
        if (!PrintUpToSeconds) {
            WriteMicroSecondsToStream(os, momentToPrint.MicroSecondsOfSecond());
        }
#ifndef _win_
        i64 utcOffsetInMinutes = localTime.tm_gmtoff / 60;
#else
        TIME_ZONE_INFORMATION tz;
        if (GetTimeZoneInformation(&tz) == TIME_ZONE_ID_INVALID) {
            ythrow TSystemError() << "Failed to get the system time zone";
        }
        i64 utcOffsetInMinutes = -tz.Bias;
#endif
        if (utcOffsetInMinutes == 0) {
            os << 'Z';
        } else {
            if (utcOffsetInMinutes < 0) {
                os << '-';
                utcOffsetInMinutes = -utcOffsetInMinutes;
            } else {
                os << '+';
            }
            os << Pad<2>(utcOffsetInMinutes / 60);
            if (iso) {
                os << ':';
            }
            os << Pad<2>(utcOffsetInMinutes % 60);
        }
    }
}

template <>
void Out<TDuration>(IOutputStream& os, TTypeTraits<TDuration>::TFuncParam duration) {
    os << duration.Seconds();
    WriteMicroSecondsToStream(os, duration.MicroSecondsOfSecond());
    os << 's';
}

template <>
void Out<TInstant>(IOutputStream& os, TTypeTraits<TInstant>::TFuncParam instant) {
    char buf[64];
    auto len = FormatDate8601(buf, sizeof(buf), instant.TimeT());

    // shouldn't happen due to current implementation of FormatDate8601() and GmTimeR()
    Y_ENSURE(len, TStringBuf("Out<TInstant>: year does not fit into an integer"));

    os.Write(buf, len - 1 /* 'Z' */);
    WriteMicroSecondsToStream(os, instant.MicroSecondsOfSecond());
    os << 'Z';
}

template <>
void Out<::NPrivate::TPrintableLocalTime<false, false>>(IOutputStream& os, TTypeTraits<::NPrivate::TPrintableLocalTime<false, false>>::TFuncParam localTime) {
    WritePrintableLocalTimeToStream(os, localTime);
}

template <>
void Out<::NPrivate::TPrintableLocalTime<false, true>>(IOutputStream& os, TTypeTraits<::NPrivate::TPrintableLocalTime<false, true>>::TFuncParam localTime) {
    WritePrintableLocalTimeToStream(os, localTime);
}

template <>
void Out<::NPrivate::TPrintableLocalTime<true, false>>(IOutputStream& os, TTypeTraits<::NPrivate::TPrintableLocalTime<true, false>>::TFuncParam localTime) {
    WritePrintableLocalTimeToStream(os, localTime);
}

template <>
void Out<::NPrivate::TPrintableLocalTime<true, true>>(IOutputStream& os, TTypeTraits<::NPrivate::TPrintableLocalTime<true, true>>::TFuncParam localTime) {
    WritePrintableLocalTimeToStream(os, localTime);
}

TString TDuration::ToString() const {
    return ::ToString(*this);
}

TString TInstant::ToString() const {
    return ::ToString(*this);
}

TString TInstant::ToRfc822String() const {
    return FormatGmTime("%a, %d %b %Y %H:%M:%S GMT");
}

TString TInstant::ToStringUpToSeconds() const {
    char buf[64];
    auto len = FormatDate8601(buf, sizeof(buf), TimeT());
    if (!len) {
        ythrow yexception() << "TInstant::ToStringUpToSeconds: year does not fit into an integer";
    }
    return TString(buf, len);
}

TString TInstant::ToIsoStringLocal() const {
    return ::ToString(FormatIsoLocal(*this));
}

TString TInstant::ToStringLocal() const {
    return ::ToString(FormatLocal(*this));
}

TString TInstant::ToRfc822StringLocal() const {
    return FormatLocalTime("%a, %d %b %Y %H:%M:%S %Z");
}

TString TInstant::ToIsoStringLocalUpToSeconds() const {
    return ::ToString(FormatIsoLocalUpToSeconds(*this));
}

TString TInstant::ToStringLocalUpToSeconds() const {
    return ::ToString(FormatLocalUpToSeconds(*this));
}

TString TInstant::FormatLocalTime(const char* format) const noexcept {
    struct tm theTm;
    LocalTime(&theTm);
    return Strftime(format, &theTm);
}

TString TInstant::FormatGmTime(const char* format) const noexcept {
    struct tm theTm;
    GmTime(&theTm);
    return Strftime(format, &theTm);
}

::NPrivate::TPrintableLocalTime<false, true> FormatIsoLocal(TInstant instant) {
    return ::NPrivate::TPrintableLocalTime<false, true>(instant);
}

::NPrivate::TPrintableLocalTime<false, false> FormatLocal(TInstant instant) {
    return ::NPrivate::TPrintableLocalTime<false, false>(instant);
}

::NPrivate::TPrintableLocalTime<true, true> FormatIsoLocalUpToSeconds(TInstant instant) {
    return ::NPrivate::TPrintableLocalTime<true, true>(instant);
}

::NPrivate::TPrintableLocalTime<true, false> FormatLocalUpToSeconds(TInstant instant) {
    return ::NPrivate::TPrintableLocalTime<true, false>(instant);
}

void Sleep(TDuration duration) {
    NanoSleep(duration.NanoSeconds());
}

void sprint_gm_date(char* buf, time_t when, long* sec) {
    struct tm theTm;
    ::Zero(theTm);
    GmTimeR(&when, &theTm);
    DateToString(buf, theTm);
    if (sec) {
        *sec = seconds(theTm);
    }
}

void DateToString(char* buf, const struct tm& theTm) {
    Y_ENSURE(0 <= theTm.tm_year + 1900 && theTm.tm_year + 1900 <= 9999, "invalid year " + ToString(theTm.tm_year + 1900) + ", year should be in range [0, 9999]");

    sprintf(buf, "%04d%02d%02d", theTm.tm_year + 1900, theTm.tm_mon + 1, theTm.tm_mday);
}

void DateToString(char* buf, time_t when, long* sec) {
    struct tm theTm;
    localtime_r(&when, &theTm);

    DateToString(buf, theTm);

    if (sec) {
        *sec = seconds(theTm);
    }
}

TString DateToString(const struct tm& theTm) {
    char buf[DATE_BUF_LEN];
    DateToString(buf, theTm);
    return buf;
}

TString DateToString(time_t when, long* sec) {
    char buf[DATE_BUF_LEN];
    DateToString(buf, when, sec);
    return buf;
}

TString YearToString(const struct tm& theTm) {
    Y_ENSURE(0 <= theTm.tm_year + 1900 && theTm.tm_year + 1900 <= 9999, "invalid year " + ToString(theTm.tm_year + 1900) + ", year should be in range [0, 9999]");
    char buf[16];
    sprintf(buf, "%04d", theTm.tm_year + 1900);
    return buf;
}

TString YearToString(time_t when) {
    struct tm theTm;
    localtime_r(&when, &theTm);

    return YearToString(theTm);
}

bool sscan_date(const char* date, struct tm& theTm) {
    int year, mon, mday;
    if (sscanf(date, "%4d%2d%2d", &year, &mon, &mday) != 3) {
        return false;
    }
    theTm.tm_year = year - 1900;
    theTm.tm_mon = mon - 1;
    theTm.tm_mday = mday;
    return true;
}

size_t FormatDate8601(char* buf, size_t len, time_t when) {
    struct tm theTm;
    struct tm* ret = GmTimeR(&when, &theTm);

    if (ret) {
        TMemoryOutput out(buf, len);

        WriteTmToStream(out, theTm);
        out << 'Z';

        return out.Buf() - buf;
    }

    return 0;
}

void SleepUntil(TInstant instant) {
    TInstant now = TInstant::Now();
    if (instant <= now) {
        return;
    }
    TDuration duration = instant - now;
    Sleep(duration);
}