const std = @import("std");
const string = []const u8;
const extras = @import("./extras.zig");
const time = @This();

pub const DateTime = struct {
    ms: u16,
    seconds: u16,
    minutes: u16,
    hours: u16,
    days: u16,
    months: u16,
    years: u16,
    timezone: TimeZone,
    weekday: WeekDay,
    era: Era,

    const Self = @This();

    pub fn initUnixMs(unix: u64) Self {
        return epoch_unix.addMs(unix);
    }

    pub fn initUnix(unix: u64) Self {
        return epoch_unix.addSecs(unix);
    }

    /// Caller asserts that this is > epoch
    pub fn init(year: u16, month: u16, day: u16, hr: u16, min: u16, sec: u16) Self {
        return epoch_unix
            .addYears(year - epoch_unix.years)
            .addMonths(month)
            .addDays(day)
            .addHours(hr)
            .addMins(min)
            .addSecs(sec);
    }

    pub fn now() Self {
        return initUnixMs(@intCast(u64, std.time.milliTimestamp()));
    }

    pub const epoch_unix = Self{
        .ms = 0,
        .seconds = 0,
        .minutes = 0,
        .hours = 0,
        .days = 0,
        .months = 0,
        .years = 1970,
        .timezone = .UTC,
        .weekday = .Thu,
        .era = .AD,
    };

    pub fn eql(self: Self, other: Self) bool {
        return self.ms == other.ms and
            self.seconds == other.seconds and
            self.minutes == other.minutes and
            self.hours == other.hours and
            self.days == other.days and
            self.months == other.months and
            self.years == other.years and
            self.timezone == other.timezone and
            self.weekday == other.weekday;
    }

    pub fn addMs(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        result.ms += @intCast(u16, count % 1000);
        return result.addSecs(count / 1000);
    }

    pub fn addSecs(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        result.seconds += @intCast(u16, count % 60);
        return result.addMins(count / 60);
    }

    pub fn addMins(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        result.minutes += @intCast(u16, count % 60);
        return result.addHours(count / 60);
    }

    pub fn addHours(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        result.hours += @intCast(u16, count % 24);
        return result.addDays(count / 24);
    }

    pub fn addDays(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        var input = count;

        while (true) {
            const year_len = result.daysThisYear();
            if (input >= year_len) {
                result.years += 1;
                input -= year_len;
                result.incrementWeekday(year_len);
                continue;
            }
            break;
        }
        while (true) {
            const month_len = result.daysThisMonth();
            if (input >= month_len) {
                result.months += 1;
                input -= month_len;
                result.incrementWeekday(month_len);

                if (result.months == 12) {
                    result.years += 1;
                    result.months = 0;
                }
                continue;
            }
            break;
        }
        {
            const month_len = result.daysThisMonth();
            if (result.days + input > month_len) {
                const left = month_len - result.days;
                input -= left;
                result.months += 1;
                result.days = 0;
                result.incrementWeekday(left);
            }
            result.days += @intCast(u16, input);
            result.incrementWeekday(input);

            if (result.months == 12) {
                result.years += 1;
                result.months = 0;
            }
        }

        return result;
    }

    pub fn addMonths(self: Self, count: u64) Self {
        if (count == 0) return self;
        var result = self;
        var input = count;
        while (input > 0) {
            const new = result.addDays(result.daysThisMonth());
            result = new;
            input -= 1;
        }
        return result;
    }

    pub fn addYears(self: Self, count: u64) Self {
        if (count == 0) return self;
        return self.addMonths(count * 12);
    }

    pub fn isLeapYear(self: Self) bool {
        return time.isLeapYear(self.years);
    }

    pub fn daysThisYear(self: Self) u16 {
        return time.daysInYear(self.years);
    }

    pub fn daysThisMonth(self: Self) u16 {
        return self.daysInMonth(self.months);
    }

    fn daysInMonth(self: Self, month: u16) u16 {
        return time.daysInMonth(self.years, month);
    }

    fn incrementWeekday(self: *Self, count: u64) void {
        var i = count % 7;
        while (i > 0) : (i -= 1) {
            self.weekday = self.weekday.next();
        }
    }

    pub fn dayOfThisYear(self: Self) u16 {
        var ret: u16 = 0;
        for (0..self.months) |item| {
            ret += self.daysInMonth(@intCast(u16, item));
        }
        ret += self.days;
        return ret;
    }

    pub fn toUnix(self: Self) u64 {
        const x = self.toUnixMilli();
        return x / 1000;
    }

    pub fn toUnixMilli(self: Self) u64 {
        var res: u64 = 0;
        res += self.ms;
        res += @as(u64, self.seconds) * std.time.ms_per_s;
        res += @as(u64, self.minutes) * std.time.ms_per_min;
        res += @as(u64, self.hours) * std.time.ms_per_hour;
        res += self.daysSinceEpoch() * std.time.ms_per_day;
        return res;
    }

    fn daysSinceEpoch(self: Self) u64 {
        var res: u64 = 0;
        res += self.days;
        for (0..self.years - epoch_unix.years) |i| res += time.daysInYear(@intCast(u16, i));
        for (0..self.months) |i| res += self.daysInMonth(@intCast(u16, i));
        return res;
    }

    /// fmt is based on https://momentjs.com/docs/#/displaying/format/
    pub fn format(self: Self, comptime fmt: string, options: std.fmt.FormatOptions, writer: anytype) !void {
        _ = options;

        if (fmt.len == 0) @compileError("DateTime: format string can't be empty");

        @setEvalBranchQuota(100000);

        comptime var s = 0;
        comptime var e = 0;
        comptime var next: ?FormatSeq = null;
        inline for (fmt, 0..) |c, i| {
            e = i + 1;

            if (comptime std.meta.stringToEnum(FormatSeq, fmt[s..e])) |tag| {
                next = tag;
                if (i < fmt.len - 1) continue;
            }

            if (next) |tag| {
                switch (tag) {
                    .MM => try writer.print("{:0>2}", .{self.months + 1}),
                    .M => try writer.print("{}", .{self.months + 1}),
                    .Mo => try printOrdinal(writer, self.months + 1),
                    .MMM => try printLongName(writer, self.months, &[_]string{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }),
                    .MMMM => try printLongName(writer, self.months, &[_]string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }),

                    .Q => try writer.print("{}", .{self.months / 3 + 1}),
                    .Qo => try printOrdinal(writer, self.months / 3 + 1),

                    .D => try writer.print("{}", .{self.days + 1}),
                    .Do => try printOrdinal(writer, self.days + 1),
                    .DD => try writer.print("{:0>2}", .{self.days + 1}),

                    .DDD => try writer.print("{}", .{self.dayOfThisYear() + 1}),
                    .DDDo => try printOrdinal(writer, self.dayOfThisYear() + 1),
                    .DDDD => try writer.print("{:0>3}", .{self.dayOfThisYear() + 1}),

                    .d => try writer.print("{}", .{@enumToInt(self.weekday)}),
                    .do => try printOrdinal(writer, @enumToInt(self.weekday)),
                    .dd => try writer.writeAll(@tagName(self.weekday)[0..2]),
                    .ddd => try writer.writeAll(@tagName(self.weekday)),
                    .dddd => try printLongName(writer, @enumToInt(self.weekday), &[_]string{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }),
                    .e => try writer.print("{}", .{@enumToInt(self.weekday)}),
                    .E => try writer.print("{}", .{@enumToInt(self.weekday) + 1}),

                    .w => try writer.print("{}", .{self.dayOfThisYear() / 7 + 1}),
                    .wo => try printOrdinal(writer, self.dayOfThisYear() / 7 + 1),
                    .ww => try writer.print("{:0>2}", .{self.dayOfThisYear() / 7 + 1}),

                    .Y => try writer.print("{}", .{self.years + 10000}),
                    .YY => try writer.print("{:0>2}", .{self.years % 100}),
                    .YYY => try writer.print("{}", .{self.years}),
                    .YYYY => try writer.print("{:0>4}", .{self.years}),

                    .N => try writer.writeAll(@tagName(self.era)),
                    .NN => try writer.writeAll("Anno Domini"),

                    .A => try printLongName(writer, self.hours / 12, &[_]string{ "AM", "PM" }),
                    .a => try printLongName(writer, self.hours / 12, &[_]string{ "am", "pm" }),

                    .H => try writer.print("{}", .{self.hours}),
                    .HH => try writer.print("{:0>2}", .{self.hours}),
                    .h => try writer.print("{}", .{wrap(self.hours, 12)}),
                    .hh => try writer.print("{:0>2}", .{wrap(self.hours, 12)}),
                    .k => try writer.print("{}", .{wrap(self.hours, 24)}),
                    .kk => try writer.print("{:0>2}", .{wrap(self.hours, 24)}),

                    .m => try writer.print("{}", .{self.minutes}),
                    .mm => try writer.print("{:0>2}", .{self.minutes}),

                    .s => try writer.print("{}", .{self.seconds}),
                    .ss => try writer.print("{:0>2}", .{self.seconds}),

                    .S => try writer.print("{}", .{self.ms / 100}),
                    .SS => try writer.print("{:0>2}", .{self.ms / 10}),
                    .SSS => try writer.print("{:0>3}", .{self.ms}),

                    .z => try writer.writeAll(@tagName(self.timezone)),
                    .Z => try writer.writeAll("+00:00"),
                    .ZZ => try writer.writeAll("+0000"),

                    .x => try writer.print("{}", .{self.toUnixMilli()}),
                    .X => try writer.print("{}", .{self.toUnix()}),
                }
                next = null;
                s = i;
            }

            switch (c) {
                ',',
                ' ',
                ':',
                '-',
                '.',
                'T',
                'W',
                => {
                    try writer.writeAll(&.{c});
                    s = i + 1;
                    continue;
                },
                else => {},
            }
        }
    }

    pub fn formatAlloc(self: Self, alloc: std.mem.Allocator, comptime fmt: string) !string {
        var list = std.ArrayList(u8).init(alloc);
        defer list.deinit();

        try self.format(fmt, .{}, list.writer());
        return list.toOwnedSlice();
    }

    const FormatSeq = enum {
        M, // 1 2 ... 11 12
        Mo, // 1st 2nd ... 11th 12th
        MM, // 01 02 ... 11 12
        MMM, // Jan Feb ... Nov Dec
        MMMM, // January February ... November December
        Q, // 1 2 3 4
        Qo, // 1st 2nd 3rd 4th
        D, // 1 2 ... 30 31
        Do, // 1st 2nd ... 30th 31st
        DD, // 01 02 ... 30 31
        DDD, // 1 2 ... 364 365
        DDDo, // 1st 2nd ... 364th 365th
        DDDD, // 001 002 ... 364 365
        d, // 0 1 ... 5 6
        do, // 0th 1st ... 5th 6th
        dd, // Su Mo ... Fr Sa
        ddd, // Sun Mon ... Fri Sat
        dddd, // Sunday Monday ... Friday Saturday
        e, // 0 1 ... 5 6 (locale)
        E, // 1 2 ... 6 7 (ISO)
        w, // 1 2 ... 52 53
        wo, // 1st 2nd ... 52nd 53rd
        ww, // 01 02 ... 52 53
        Y, // 11970 11971 ... 19999 20000 20001 (Holocene calendar)
        YY, // 70 71 ... 29 30
        YYY, // 1 2 ... 1970 1971 ... 2029 2030
        YYYY, // 0001 0002 ... 1970 1971 ... 2029 2030
        N, // BC AD
        NN, // Before Christ ... Anno Domini
        A, // AM PM
        a, // am pm
        H, // 0 1 ... 22 23
        HH, // 00 01 ... 22 23
        h, // 1 2 ... 11 12
        hh, // 01 02 ... 11 12
        k, // 1 2 ... 23 24
        kk, // 01 02 ... 23 24
        m, // 0 1 ... 58 59
        mm, // 00 01 ... 58 59
        s, // 0 1 ... 58 59
        ss, // 00 01 ... 58 59
        S, // 0 1 ... 8 9 (second fraction)
        SS, // 00 01 ... 98 99
        SSS, // 000 001 ... 998 999
        z, // EST CST ... MST PST
        Z, // -07:00 -06:00 ... +06:00 +07:00
        ZZ, // -0700 -0600 ... +0600 +0700
        x, // unix milli
        X, // unix
    };

    pub fn since(self: Self, other_in_the_past: Self) Duration {
        return Duration{
            .ms = self.toUnixMilli() - other_in_the_past.toUnixMilli(),
        };
    }
};

pub const format = struct {
    pub const LT = "";
    pub const LTS = "";
    pub const L = "";
    pub const l = "";
    pub const LL = "";
    pub const ll = "";
    pub const LLL = "";
    pub const lll = "";
    pub const LLLL = "";
    pub const llll = "";
};

pub const TimeZone = enum {
    UTC,

    usingnamespace extras.TagNameJsonStringifyMixin(@This());
};

pub const WeekDay = enum {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,

    pub fn next(self: WeekDay) WeekDay {
        return switch (self) {
            .Sun => .Mon,
            .Mon => .Tue,
            .Tue => .Wed,
            .Wed => .Thu,
            .Thu => .Fri,
            .Fri => .Sat,
            .Sat => .Sun,
        };
    }

    usingnamespace extras.TagNameJsonStringifyMixin(@This());
};

pub const Era = enum {
    // BC,
    AD,

    usingnamespace extras.TagNameJsonStringifyMixin(@This());
};

pub fn isLeapYear(year: u16) bool {
    var ret = false;
    if (year % 4 == 0) ret = true;
    if (year % 100 == 0) ret = false;
    if (year % 400 == 0) ret = true;
    return ret;
}

pub fn daysInYear(year: u16) u16 {
    return if (isLeapYear(year)) 366 else 365;
}

fn daysInMonth(year: u16, month: u16) u16 {
    const norm = [12]u16{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    const leap = [12]u16{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    const month_days = if (!isLeapYear(year)) norm else leap;
    return month_days[month];
}

fn printOrdinal(writer: anytype, num: u16) !void {
    try writer.print("{}", .{num});
    try writer.writeAll(switch (num) {
        1 => "st",
        2 => "nd",
        3 => "rd",
        else => "th",
    });
}

fn printLongName(writer: anytype, index: u16, names: []const string) !void {
    try writer.writeAll(names[index]);
}

fn wrap(val: u16, at: u16) u16 {
    var tmp = val % at;
    return if (tmp == 0) at else tmp;
}

pub const Duration = struct {
    ms: u64,
};
