#undef PTIME_TZPARSE_HEADERFUNC
#undef PTIME_TZPARSE_BODYFUNC
#undef PTIME_TZPARSE_TRANSTIME_TYPE
#undef PTIME_TZPARSE_LEAPSEC_TYPE
#undef PTIME_TZPARSE_LEAPSEC_SIZE
#undef PTIME_TZPARSE_NTOH_DYN

#ifdef PTIME_TZPARSE_V2
#  define PTIME_TZPARSE_HEADERFUNC     tzparse_headerV2
#  define PTIME_TZPARSE_BODYFUNC       tzparse_bodyV2
#  define PTIME_TZPARSE_TRANSTIME_TYPE ftz_transtimeV2
#  define PTIME_TZPARSE_LEAPSEC_TYPE   ftz_leapsecV2
#  define PTIME_TZPARSE_NTOH_DYN(val)  ((int64_t)panda::be2h64(val))
#else
#  define PTIME_TZPARSE_HEADERFUNC     tzparse_headerV1
#  define PTIME_TZPARSE_BODYFUNC       tzparse_bodyV1
#  define PTIME_TZPARSE_TRANSTIME_TYPE ftz_transtimeV1
#  define PTIME_TZPARSE_LEAPSEC_TYPE   ftz_leapsecV1
#  define PTIME_TZPARSE_NTOH_DYN(val)  ((int32_t)panda::be2h32(val))
#endif

#ifndef PTIME_TZPARSE_TRANSCMP
# define PTIME_TZPARSE_TRANSCMP
static int trans_cmp (const void* _a, const void* _b) {
    Timezone::Transition* a = (Timezone::Transition*) _a;
    Timezone::Transition* b = (Timezone::Transition*) _b;
    if (a->start < b->start) return -1;
    else if (a->start == b->start) return 0;
    else return 1;
}
#endif

#define PTIME_TZPARSE_BODY_BASE_SIZE(head) (                                                            \
    head.tzh_timecnt*sizeof(PTIME_TZPARSE_TRANSTIME_TYPE) + /* transition times */                      \
    head.tzh_timecnt*sizeof(ftz_ilocaltype) +               /* types of local time starting at above */ \
    head.tzh_typecnt*sizeof(ftz_localtype) +                /* local times */                           \
    head.tzh_charcnt +                                      /* abbrevs */                               \
    head.tzh_leapcnt*sizeof(PTIME_TZPARSE_LEAPSEC_TYPE) +   /* leap seconds */                          \
    head.tzh_ttisstdcnt*sizeof(ftz_isstd) +                                                             \
    head.tzh_ttisgmtcnt*sizeof(ftz_isgmt)                                                               \
)

static inline int PTIME_TZPARSE_HEADERFUNC (const char*& ptr, const char*const end, ftz_head& head, int* version) {
    if (ptr + sizeof(head) > end) return -1;
    head = *((ftz_head*)ptr);
    ptr += sizeof(head);
    
    if (strncmp(head.tzh_magic, FTZ_MAGIC, sizeof(head.tzh_magic)) != 0) {
        //fprintf(stderr, "ptime: BAD FILE MAGIC\n", head.tzh_magic);
        return -1;
    }
    
    *version = head.tzh_version[0] ? (head.tzh_version[0] - '0') : 1;
    
    head.tzh_ttisgmtcnt = panda::be2h32(head.tzh_ttisgmtcnt);
    head.tzh_ttisstdcnt = panda::be2h32(head.tzh_ttisstdcnt);
    head.tzh_leapcnt    = panda::be2h32(head.tzh_leapcnt);
    head.tzh_timecnt    = panda::be2h32(head.tzh_timecnt);
    head.tzh_typecnt    = panda::be2h32(head.tzh_typecnt);
    head.tzh_charcnt    = panda::be2h32(head.tzh_charcnt);
    
    if (head.tzh_timecnt > FTZ_MAX_TIMES) {
        //fprintf(stderr, "ptime: tzh_timecnt %d is greater than max supported %d\n", head.tzh_timecnt, FTZ_MAX_TIMES);
        return -1;
    }
    
    if (head.tzh_typecnt > FTZ_MAX_TYPES) {
        //fprintf(stderr, "ptime: tzh_typecnt %d is greater than max supported %d\n", head.tzh_typecnt, FTZ_MAX_TYPES);
        return -1;
    }
    
    if (head.tzh_charcnt > FTZ_MAX_CHARS) {
        //fprintf(stderr, "ptime: tzh_charcnt %d is greater than max supported %d\n", head.tzh_charcnt, FTZ_MAX_CHARS);
        return -1;
    }
    
    if (head.tzh_leapcnt > FTZ_MAX_LEAPS) {
        //fprintf(stderr, "ptime: tzh_leapcnt %d is greater than max supported %d\n", head.tzh_leapcnt, FTZ_MAX_LEAPS);
        return -1;
    }

    return PTIME_TZPARSE_BODY_BASE_SIZE(head);
}

static inline bool PTIME_TZPARSE_BODYFUNC (const char*& ptr, const char*const end, ftz_head& head, Timezone* zone) {
    if (ptr + PTIME_TZPARSE_BODY_BASE_SIZE(head) > end) return false;
    
    auto transitions = ptr;
    ptr += head.tzh_timecnt * sizeof(PTIME_TZPARSE_TRANSTIME_TYPE);

    ftz_ilocaltype* ilocaltypes = (ftz_ilocaltype*)ptr;
    ptr += head.tzh_timecnt * sizeof(ftz_ilocaltype);

    ftz_localtype localtypes[FTZ_MAX_TYPES];
    for (uint32_t i = 0; i < head.tzh_typecnt; i++) {
        localtypes[i] = *((ftz_localtype*)ptr);
        localtypes[i].offset = panda::be2h32(localtypes[i].offset);
        ptr += sizeof(ftz_localtype);
    }

    const char* abbrevs = ptr;
    ptr += head.tzh_charcnt * sizeof(char);

    zone->leaps_cnt = head.tzh_leapcnt;
    zone->leaps = zone->leaps_cnt > 0 ? new Timezone::Leap[zone->leaps_cnt] : NULL;
    for (uint32_t i = 0; i < head.tzh_leapcnt; i++) {
        PTIME_TZPARSE_LEAPSEC_TYPE leapsec = *((PTIME_TZPARSE_LEAPSEC_TYPE*)ptr);
        zone->leaps[i].time       = (ptime_t)PTIME_TZPARSE_NTOH_DYN(leapsec.time);
        zone->leaps[i].correction = panda::be2h32(leapsec.correction);
        ptr += sizeof(leapsec);
    }

    //ftz_isstd* isstds = (ftz_isstd*)ptr;
    ptr += head.tzh_ttisstdcnt * sizeof(ftz_isstd);

    //ftz_isgmt* isgmts = (ftz_isgmt*)ptr;
    ptr += head.tzh_ttisgmtcnt * sizeof(ftz_isgmt);
    
    // find past localtype - first localtype if it's not used in transitions, otherwise it's first std time localtype
    int past_lt_index = 0;
    for (uint32_t i = 0; i < head.tzh_timecnt; ++i) {
        if (ilocaltypes[i] != 0) continue;
        past_lt_index = -1;
        break;
    }
    if (past_lt_index < 0) for (uint32_t i = 0; i < head.tzh_typecnt; ++i) {
        if (localtypes[i].isdst) continue;
        past_lt_index = i;
        break;
    }
    if (past_lt_index < 0) past_lt_index = 0;
    
    zone->trans_cnt = head.tzh_timecnt + 1 + zone->leaps_cnt; // +1 for 'past'
    zone->trans = new Timezone::Transition[zone->trans_cnt];
    std::memset(zone->trans, 0, zone->trans_cnt * sizeof(*zone->trans));
    
    zone->trans[0].start       = EPOCH_NEGINF;
    zone->trans[0].local_start = EPOCH_NEGINF;
    zone->trans[0].local_lower = EPOCH_NEGINF;
    zone->trans[0].local_upper = EPOCH_NEGINF;
    zone->trans[0].offset      = localtypes[past_lt_index].offset;
    zone->trans[0].gmt_offset  = localtypes[past_lt_index].offset;
    zone->trans[0].delta       = 0;
    zone->trans[0].isdst       = localtypes[past_lt_index].isdst;
    zone->trans[0].leap_corr   = 0;
    zone->trans[0].leap_delta  = 0;
    zone->trans[0].leap_end    = EPOCH_NEGINF;
    zone->trans[0].leap_lend   = EPOCH_NEGINF;
    const char* past_abbrev    = abbrevs + localtypes[past_lt_index].abbrev_offset;
    if (strlen(past_abbrev) > ZONE_ABBR_MAX) {
        //fprintf(stderr, "ptime: past abbrev is too long (%d), max is %d\n", strlen(past_abbrev), ZONE_ABBR_MAX);
        zone->clear();
        return false;
    }
    strcpy(zone->trans[0].abbrev, past_abbrev);

    for (uint32_t i = 0; i < head.tzh_timecnt; ++i) {
        ftz_localtype localtype = localtypes[ilocaltypes[i]];
        auto this_trans         = &zone->trans[i+1];
        
        PTIME_TZPARSE_TRANSTIME_TYPE tmp1;
        memcpy(&tmp1, transitions, sizeof(tmp1));
        transitions += sizeof(tmp1);
        this_trans->start       = (ptime_t)PTIME_TZPARSE_NTOH_DYN(tmp1);
        
        this_trans->gmt_offset  = localtype.offset;
        this_trans->isdst       = localtype.isdst;
        const char* abbrev      = abbrevs + localtype.abbrev_offset;
        if (strlen(abbrev) > ZONE_ABBR_MAX) {
            //fprintf(stderr, "ptime: locatype's #%d abbrev is too long (%d), max is %d\n", ilocaltypes[i], strlen(abbrev), ZONE_ABBR_MAX);
            zone->clear();
            return false;
        }
        strcpy(this_trans->abbrev, abbrev);
    }
    
    for (uint32_t i = 0; i < zone->leaps_cnt; i++) {
        auto this_trans       = &zone->trans[head.tzh_timecnt+i+1];
        this_trans->start     = zone->leaps[i].time;
        this_trans->leap_corr = zone->leaps[i].correction;
    }
    
    qsort(zone->trans, zone->trans_cnt, sizeof(*zone->trans), trans_cmp);
    
    for (uint32_t i = 1; i < zone->trans_cnt; ++i) {
        auto this_trans = &zone->trans[i];
        auto prev_trans = &zone->trans[i-1];
        
        if (this_trans->leap_corr != 0) {
            this_trans->leap_delta = this_trans->leap_corr - prev_trans->leap_corr;
            this_trans->gmt_offset = prev_trans->gmt_offset;
            this_trans->isdst      = prev_trans->isdst;
            strcpy(this_trans->abbrev, prev_trans->abbrev);
        } else {
            this_trans->leap_delta = 0;
            this_trans->leap_corr  = prev_trans->leap_corr;
        }
        
        this_trans->offset      = this_trans->gmt_offset - this_trans->leap_corr;
        this_trans->delta       = this_trans->offset - prev_trans->offset;
        this_trans->local_start = this_trans->start + this_trans->offset;
        prev_trans->local_end   = this_trans->start + prev_trans->offset;
        this_trans->local_lower = this_trans->local_start;
        this_trans->local_upper = std::max(this_trans->local_start, prev_trans->local_end);
        this_trans->leap_end    = this_trans->start + this_trans->leap_delta;
        this_trans->leap_lend   = this_trans->local_start + 2*this_trans->leap_delta;
    }

    
    const char* posixstr = ptr;
    const char* posixend = ptr;
    
#ifdef PTIME_TZPARSE_V2
    posixstr++; // because POSIX rule begins and ends with '\n'
    if (posixstr >= end) { zone->clear(); return false; }
    posixend = (const char*)std::memchr(posixstr, '\n', end - posixstr);
    if (posixend == NULL) { zone->clear(); return false; }
#endif

    zone->ltrans = zone->trans[zone->trans_cnt-1];

    if (posixend == posixstr) { // no posix string, using last transition
        zone->future.hasdst           = 0;
        zone->future.outer.gmt_offset = zone->ltrans.gmt_offset;
        zone->future.outer.isdst      = zone->ltrans.isdst;
        strcpy(zone->future.outer.abbrev, zone->ltrans.abbrev);
    }
    else if (!tzparse_rule(string_view(posixstr, posixend-posixstr), &zone->future)) {
        //fprintf(stderr, "ptime: tzparse_rule failed\n");
        zone->clear();
        return false;
    }

    zone->future.outer.offset = zone->future.outer.gmt_offset - zone->ltrans.leap_corr;
    if (zone->future.hasdst) {
        zone->future.inner.offset = zone->future.inner.gmt_offset - zone->ltrans.leap_corr;
        zone->future.delta        = zone->future.inner.offset - zone->future.outer.offset;
        zone->future.max_offset   = std::max(zone->future.outer.offset, zone->future.inner.offset);
    }
    
    return true;
}