const std = @import("std.zig");
const builtin = @import("builtin");
pub const Transition = struct {
    ts: i64,
    timetype: *Timetype,
};
pub const Timetype = struct {
    offset: i32,
    flags: u8,
    name_data: [6:0]u8,
    pub fn name(self: *const Timetype) [:0]const u8 {
        return std.mem.sliceTo(self.name_data[0..], 0);
    }
    pub fn isDst(self: Timetype) bool {
        return (self.flags & 0x01) > 0;
    }
    pub fn standardTimeIndicator(self: Timetype) bool {
        return (self.flags & 0x02) > 0;
    }
    pub fn utIndicator(self: Timetype) bool {
        return (self.flags & 0x04) > 0;
    }
};
pub const Leapsecond = struct {
    occurrence: i48,
    correction: i16,
};
pub const Tz = struct {
    allocator: std.mem.Allocator,
    transitions: []const Transition,
    timetypes: []const Timetype,
    leapseconds: []const Leapsecond,
    footer: ?[]const u8,
    const Header = extern struct {
        magic: [4]u8,
        version: u8,
        reserved: [15]u8,
        counts: extern struct {
            isutcnt: u32,
            isstdcnt: u32,
            leapcnt: u32,
            timecnt: u32,
            typecnt: u32,
            charcnt: u32,
        },
    };
    pub fn parse(allocator: std.mem.Allocator, reader: anytype) !Tz {
        var legacy_header = try reader.readStruct(Header);
        if (!std.mem.eql(u8, &legacy_header.magic, "TZif")) return error.BadHeader;
        if (legacy_header.version != 0 and legacy_header.version != '2' and legacy_header.version != '3') return error.BadVersion;
        if (builtin.target.cpu.arch.endian() != std.builtin.Endian.Big) {
            std.mem.byteSwapAllFields(@TypeOf(legacy_header.counts), &legacy_header.counts);
        }
        if (legacy_header.version == 0) {
            return parseBlock(allocator, reader, legacy_header, true);
        } else {
            
            const skipv = legacy_header.counts.timecnt * 5 + legacy_header.counts.typecnt * 6 + legacy_header.counts.charcnt + legacy_header.counts.leapcnt * 8 + legacy_header.counts.isstdcnt + legacy_header.counts.isutcnt;
            try reader.skipBytes(skipv, .{});
            var header = try reader.readStruct(Header);
            if (!std.mem.eql(u8, &header.magic, "TZif")) return error.BadHeader;
            if (header.version != '2' and header.version != '3') return error.BadVersion;
            if (builtin.target.cpu.arch.endian() != std.builtin.Endian.Big) {
                std.mem.byteSwapAllFields(@TypeOf(header.counts), &header.counts);
            }
            return parseBlock(allocator, reader, header, false);
        }
    }
    fn parseBlock(allocator: std.mem.Allocator, reader: anytype, header: Header, legacy: bool) !Tz {
        if (header.counts.isstdcnt != 0 and header.counts.isstdcnt != header.counts.typecnt) return error.Malformed; 
        if (header.counts.isutcnt != 0 and header.counts.isutcnt != header.counts.typecnt) return error.Malformed; 
        if (header.counts.typecnt == 0) return error.Malformed; 
        if (header.counts.charcnt == 0) return error.Malformed; 
        if (header.counts.charcnt > 256 + 6) return error.Malformed; 
        var leapseconds = try allocator.alloc(Leapsecond, header.counts.leapcnt);
        errdefer allocator.free(leapseconds);
        var transitions = try allocator.alloc(Transition, header.counts.timecnt);
        errdefer allocator.free(transitions);
        var timetypes = try allocator.alloc(Timetype, header.counts.typecnt);
        errdefer allocator.free(timetypes);
        
        var i: usize = 0;
        while (i < header.counts.timecnt) : (i += 1) {
            transitions[i].ts = if (legacy) try reader.readIntBig(i32) else try reader.readIntBig(i64);
        }
        i = 0;
        while (i < header.counts.timecnt) : (i += 1) {
            const tt = try reader.readByte();
            if (tt >= timetypes.len) return error.Malformed; 
            transitions[i].timetype = &timetypes[tt];
        }
        
        i = 0;
        while (i < header.counts.typecnt) : (i += 1) {
            const offset = try reader.readIntBig(i32);
            if (offset < -2147483648) return error.Malformed; 
            const dst = try reader.readByte();
            if (dst != 0 and dst != 1) return error.Malformed; 
            const idx = try reader.readByte();
            if (idx > header.counts.charcnt - 1) return error.Malformed; 
            timetypes[i] = .{
                .offset = offset,
                .flags = dst,
                .name_data = undefined,
            };
            
            timetypes[i].name_data[0] = idx;
        }
        var designators_data: [256 + 6]u8 = undefined;
        try reader.readNoEof(designators_data[0..header.counts.charcnt]);
        const designators = designators_data[0..header.counts.charcnt];
        if (designators[designators.len - 1] != 0) return error.Malformed; 
        
        for (timetypes) |*tt| {
            const name = std.mem.sliceTo(designators[tt.name_data[0]..], 0);
            
            if (name.len > 6) return error.Malformed; 
            std.mem.copy(u8, tt.name_data[0..], name);
            tt.name_data[name.len] = 0;
        }
        
        i = 0;
        while (i < header.counts.leapcnt) : (i += 1) {
            const occur: i64 = if (legacy) try reader.readIntBig(i32) else try reader.readIntBig(i64);
            if (occur < 0) return error.Malformed; 
            if (i > 0 and leapseconds[i - 1].occurrence + 2419199 > occur) return error.Malformed; 
            if (occur > std.math.maxInt(i48)) return error.Malformed; 
            const corr = try reader.readIntBig(i32);
            if (i == 0 and corr != -1 and corr != 1) return error.Malformed; 
            if (i > 0 and leapseconds[i - 1].correction != corr + 1 and leapseconds[i - 1].correction != corr - 1) return error.Malformed; 
            if (corr > std.math.maxInt(i16)) return error.Malformed; 
            leapseconds[i] = .{
                .occurrence = @intCast(i48, occur),
                .correction = @intCast(i16, corr),
            };
        }
        
        i = 0;
        while (i < header.counts.isstdcnt) : (i += 1) {
            const stdtime = try reader.readByte();
            if (stdtime == 1) {
                timetypes[i].flags |= 0x02;
            }
        }
        
        i = 0;
        while (i < header.counts.isutcnt) : (i += 1) {
            const ut = try reader.readByte();
            if (ut == 1) {
                timetypes[i].flags |= 0x04;
                if (!timetypes[i].standardTimeIndicator()) return error.Malformed; 
            }
        }
        
        var footer: ?[]u8 = null;
        if (!legacy) {
            if ((try reader.readByte()) != '\n') return error.Malformed; 
            var footerdata_buf: [128]u8 = undefined;
            const footer_mem = reader.readUntilDelimiter(&footerdata_buf, '\n') catch |err| switch (err) {
                error.StreamTooLong => return error.OverlargeFooter, 
                else => return err,
            };
            if (footer_mem.len != 0) {
                footer = try allocator.dupe(u8, footer_mem);
            }
        }
        errdefer if (footer) |ft| allocator.free(ft);
        return Tz{
            .allocator = allocator,
            .transitions = transitions,
            .timetypes = timetypes,
            .leapseconds = leapseconds,
            .footer = footer,
        };
    }
    pub fn deinit(self: *Tz) void {
        if (self.footer) |footer| {
            self.allocator.free(footer);
        }
        self.allocator.free(self.leapseconds);
        self.allocator.free(self.transitions);
        self.allocator.free(self.timetypes);
    }
};
test "slim" {
    const data = @embedFile("tz/asia_tokyo.tzif");
    var in_stream = std.io.fixedBufferStream(data);
    var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
    defer tz.deinit();
    try std.testing.expectEqual(tz.transitions.len, 9);
    try std.testing.expect(std.mem.eql(u8, tz.transitions[3].timetype.name(), "JDT"));
    try std.testing.expectEqual(tz.transitions[5].ts, -620298000); 
    try std.testing.expectEqual(tz.leapseconds[13].occurrence, 567993613); 
}
test "fat" {
    const data = @embedFile("tz/antarctica_davis.tzif");
    var in_stream = std.io.fixedBufferStream(data);
    var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
    defer tz.deinit();
    try std.testing.expectEqual(tz.transitions.len, 8);
    try std.testing.expect(std.mem.eql(u8, tz.transitions[3].timetype.name(), "+05"));
    try std.testing.expectEqual(tz.transitions[4].ts, 1268251224); 
}
test "legacy" {
    
    const data = @embedFile("tz/europe_vatican.tzif");
    var in_stream = std.io.fixedBufferStream(data);
    var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
    defer tz.deinit();
    try std.testing.expectEqual(tz.transitions.len, 170);
    try std.testing.expect(std.mem.eql(u8, tz.transitions[69].timetype.name(), "CET"));
    try std.testing.expectEqual(tz.transitions[123].ts, 1414285200); 
}