/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

/**
 * @file
 *
 * This file defines TimeZone related classes.
 */

package std.time

import std.convert.*


@When[os == "Windows"]
const SEPARATOR = ";"
@When[os == "Windows"]
const SLASH_CHAR = "\\"

@When[os != "Windows"]
const SEPARATOR = ":"
@When[os != "Windows"]
const SLASH_CHAR = "/"

let TZIF_SOURCE: Array<String> = ["/usr/share/zoneinfo", "/system/etc/zoneinfo", "/system/usr/share/zoneinfo/"]

let ANDROID_TZDATA = "/system/usr/share/zoneinfo/tzdata"

const TZIF_SOUTCE_LINUX = "/usr/share/zoneinfo/"

const LOCALTIME_SRC: String = "/etc/localtime"

const TZPATH = "CJ_TZPATH"

const MAXPATHLEN = 4096

@FastNative
foreign func CJ_TIME_GetEnvVariable(id: CString): CString

enum RuleType {
    | JulianDay /* Jn = Julian day */
    | DayOfYear /* n = day of year */
    | MonthNthDayOfWeek /* Mm.n.d = month, week, day of week */
}

class ZoneRule {
    var rType: RuleType = JulianDay /* type of rule */
    var rDay: Int64 = 0 /* day number of rule */
    var rWeek: Int64 = 0 /* week number of rule */
    var rMon: Int64 = 0 /* month number of rule */
    var rTime: Int64 = 0 /* transition time of rule */

    func parse(footer: String): String {
        if (footer[0] == b'J') {
            let (jday, rest) = getNum(footer[1..])
            if (jday < 1 || jday > 365) {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            this.rType = JulianDay
            this.rDay = jday
            return rest
        } else if (footer[0] == b'M') {
            var (mon, week, day) = (0, 0, 0)
            var rest = footer
            (mon, rest) = getNum(rest[1..])
            // Months range from 1 to 12
            if (mon < 1 || mon > 12 || rest.size == 0 || rest[0] != b'.') {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            (week, rest) = getNum(rest[1..])
            // The range of weeks in a month is 1 to 5.
            if (week < 1 || week > 5 || rest.size == 0 || rest[0] != b'.') {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            (day, rest) = getNum(rest[1..])
            // The number of days in a week ranges from 0 to 6.
            if (day < 0 || day > 6) {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            this.rType = MonthNthDayOfWeek
            this.rDay = day
            this.rWeek = week
            this.rMon = mon
            return rest
        } else {
            let (day, rest) = getNum(footer)
            if (day < 0 || day > 365) {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            this.rType = DayOfYear
            this.rDay = day
            return rest
        }
    }
}

class LocalTime {
    /* time zone designation. */
    var des: String = ""

    /* time zone offset. */
    var offset: Int32 = 0

    /* Indicates whether local time should be considered Daylight Saving Time (DST). */
    var isDST: Bool = false

    init() {}

    init(des: String, offset: Int32, isDST: Bool) {
        this.des = des
        this.offset = offset
        this.isDST = isDST
    }
}

class TransTime {
    /* UNIX leap-time values, used as a transition time at which the rules for computing local time may change. */
    var trans: Int64 = Int64.Min

    /*
     * Specify the type of local time of the corresponding transition time.
     * This value serves as zero-based indices into the array of LocalTime.
     */
    var index: UInt8 = 0

    init() {}

    init(trans: Int64, index: UInt8) {
        this.trans = trans
        this.index = index
    }
}

/**
 * The class TimeZone implements the basic functions of the time zone.
 * The time zone data comes from the IANA time zone database file installed in the operating system.
 * The local time zone is determined by the environment variable TZ.
 * If the TZ does not exist, it is determined by the time zone stored in the /etc/localtime file.
 * Otherwise, the host is regarded as the UTC time zone.
 *
 * @since 0.19.3
 */
public class TimeZone <: ToString & Equatable<TimeZone> {
    public static let UTC: TimeZone = TimeZone("UTC", 0)
    public static let Local: TimeZone = initLocal()

    let zoneId: String
    let localTimes: Array<LocalTime>
    let transTimes: Array<TransTime>
    let footer: String /* TZif footer. */

    /* The cache of the last successful lookup, including the start, end, and corresponding zone. */
    var cache: Option<(Int64, Int64, LocalTime)> = None

    public prop id: String {
        get() {
            this.zoneId
        }
    }

    /**
     * Constructs a fixed TimeZone that always uses the given zone id and offset (seconds east of UTC).
     *
     * @param id zone id.
     * @param offset zone offset (seconds east of UTC).
     *
     * @since 0.19.3
     */
    init(id: String, offset: Int32) {
        this.zoneId = id
        this.localTimes = Array<LocalTime>(1, repeat: LocalTime(id, offset, false))
        this.transTimes = Array<TransTime>(1, repeat: TransTime())
        this.footer = ""
    }

    /**
     * Constructs a fixed TimeZone with given zone id and offset.
     *
     * @param id zone id.
     * @param offset zone offset.
     *
     * @throws IllegalArgumentException if TimeZone id is empty.
     * @since 0.19.3
     */
    public init(id: String, offset: Duration) {
        if (id.size == 0) {
            throw IllegalArgumentException("Invalid timezone id.")
        }
        let second = offset.toSeconds()
        if (second < MIN_OFFSET || second > MAX_OFFSET) {
            throw IllegalArgumentException("Invalid offset.")
        }
        this.zoneId = id
        this.localTimes = Array<LocalTime>(1, repeat: LocalTime(id, Int32(second), false))
        this.transTimes = Array<TransTime>(1, repeat: TransTime())
        this.footer = ""
    }

    /*
     * Constructs a TimeZone instance using the given arguments.
     *
     * @param localTimes an array of localTime.
     * @param transTimes an array of zone transition time.
     * @param id zone id.
     * @param footer extend zone id.
     *
     * @throws IllegalArgumentException if TimeZone id is empty.
     * @since 0.19.3
     */
    private init(localTimes: Array<LocalTime>, transTimes: Array<TransTime>, id: String, footer: String) {
        this.zoneId = id
        this.localTimes = localTimes
        this.transTimes = transTimes
        this.footer = footer
        this.cache = (Int64.Min, Int64.Max, localTimes[0])
    }

    /**
     * Loads a TimeZone with the given id.
     * If the id is "UTC", it returns TimeZone.UTC. If the id is "Local", it returns TimeZone.Local.
     * Otherwise, the id is considered the TimeZone id corresponding to the file in the IANA
     * If the CJ_TZPATH environment variable exists, obtain the path from the environment variable first.
     * time zone database, such as "Asia/Shanghai".
     *
     * @param id zone id.
     * @return the TimeZone if success, otherwise an Exception.
     *
     * @throws IllegalArgumentException if TimeZone id is illegal or empty.
     * @throws InvalidDataException if the timezone data cannot be found when a valid environment variable is set
     * @throws InvalidDataException if the TimeZone file fails to be parsed.
     * @throws IllegalMemoryException if failed to call loadTimeZone(cjvm).
     *
     * @since 0.19.3
     */
    public static func load(id: String): TimeZone {
        if (id.size == 0 || id.size > MAXPATHLEN) {
            throw IllegalArgumentException("Invalid timezone id.")
        }
        if (id == "UTC") {
            return UTC
        }
        if (id == "Local") {
            return Local
        }
        if (id.contains("..") || id.contains("\0") || id.contains("\\") || id[0] == b'/' || id[0] == b'\\') {
            throw IllegalArgumentException("Invalid timezone id: ${id}.")
        }
        if (let Some(tzdata) <- getOhosTzDataById(id)) {
            return TimeZone.loadFromTZData(id, tzdata)
        }
        var env = unsafe { LibC.mallocCString(TZPATH) }
        let envPath = unsafe { CJ_TIME_GetEnvVariable(env) }
        unsafe { LibC.free(env) }
        var envPathArray = envPath.toString().split(SEPARATOR, removeEmpty: true)
        if (envPathArray.size == 0) {
            return loadFromPaths(id, TZIF_SOURCE)
        }
        return loadFromPaths(id, envPathArray)
    }

    /**
     * Obtains the TimeZone object based on the given root path and time zone id.
     * This function searches for the timezone data named <id> from the given root path in turn until it is found.

     * @param tzpaths Set of root paths for timezone datas.
     * @param id zone id.
     * @return the TimeZone if success, otherwise an Exception.
     *
     * @throws IllegalArgumentException if TimeZone id is empty.
     * @throws InvalidDataException if the timezone data cannot be found.
     * @throws InvalidDataException if the TimeZone file fails to be parsed.
     * @throws IllegalMemoryException if failed to call loadTimeZone(cjvm).
     *
     * @since 0.35.7
     */
    public static func loadFromPaths(id: String, tzpaths: Array<String>): TimeZone {
        if (id.size == 0 || id.size > MAXPATHLEN) {
            throw IllegalArgumentException("Invalid timezone id.")
        }
        if (id.contains("..") || id.contains("\0") || id[0] == b'/' || id[0] == b'\\') {
            throw IllegalArgumentException("Invalid timezone name.")
        }

        for (tzpath in tzpaths) {
            if (tzpath.size > MAXPATHLEN) {
                continue
            }

            var absPath = tzpath + SLASH_CHAR + id
            let pathLen: Int64 = absPath.size
            unsafe {
                let cPath: CPointerHandle<UInt8> = acquireArrayRawData(absPath.rawData())
                let fileSize = CJ_TIME_GetFileSize(cPath.pointer, pathLen)
                releaseArrayRawData(cPath)
                if (fileSize < 0) {
                    continue
                }
                return loadTimeZone(id, tzpath)
            }
        }
        unsafe {
            let cPath: CPointerHandle<UInt8> = acquireArrayRawData(ANDROID_TZDATA.rawData())
            let pathLen: Int64 = ANDROID_TZDATA.size
            let fileSize = CJ_TIME_GetFileSize(cPath.pointer, pathLen)
            releaseArrayRawData(cPath)
            if (fileSize > 0) {
                return loadTimeZoneFromTzData(id, ANDROID_TZDATA)
            }
        }
        throw InvalidDataException("No valid timezone file is found.")
    }

    /**
     * Loads a TimeZone with the given id initialized from the IANA Time LocalTime database format data.
     *
     * @param id zone id.
     * @param data IANA Time LocalTime database format data.
     * @return the TimeZone if success, otherwise an Exception.
     *
     * @throws IllegalArgumentException if id is empty.
     * @throws InvalidDataException if the TimeZone data fails to be parsed.
     * @throws IllegalMemoryException if failed to call runtimeNow(cjvm).
     * @since 0.19.3
     */
    public static func loadFromTZData(id: String, data: Array<UInt8>): TimeZone {
        if (id.size == 0) {
            throw IllegalArgumentException("Invalid timezone id.")
        }
        var (tzifLocalTimes, tzifTransTimes, tzifFooter) = parseTZinfoData(data)
        var result: TimeZone = TimeZone(tzifLocalTimes, tzifTransTimes, id, tzifFooter)
        var maxIndex: UInt8 = 0
        for (i in 0..tzifTransTimes.size) {
            if (tzifTransTimes[i].index > maxIndex) {
                maxIndex = tzifTransTimes[i].index
            }
        }
        if (Int64(maxIndex) >= tzifLocalTimes.size) {
            throw InvalidDataException("Failed to parse the timezone file.")
        }
        var temp = result.transTimes
        var (sec, _) = systemNow()
        for (i in 0..temp.size) {
            if (temp[i].trans <= sec && (i + 1 == temp.size || sec < temp[i + 1].trans)) {
                let start = temp[i].trans
                var end = Int64.Max
                if (i + 1 < temp.size) {
                    end = temp[i + 1].trans
                }
                let zone = result.localTimes[Int64(temp[i].index)]
                result.cache = (start, end, zone)
            }
        }
        return result
    }

    /**
     * Return a String that represents the time zone, actually time zone id.
     *
     * @return time zone id.
     *
     * @since 0.19.3
     */
    public func toString(): String {
        return this.zoneId
    }

    /**
     * Override the == operator, determines whether this TimeZone equals another TimeZone.
     *
     * @param r another TimeZone.
     * @return true if the TimeZone is equal to @p r, otherwise false.
     *
     * @since 0.38.2
     */
    public operator func ==(r: TimeZone): Bool {
        return refEq(this, r)
    }

    /**
     * Override the == operator, determines whether this TimeZone is not equal another TimeZone.
     *
     * @param r another TimeZone.
     * @return true if the TimeZone is not equal to @p r, otherwise false.
     *
     * @since 0.38.2
     */
    public operator func !=(r: TimeZone): Bool {
        return !(this == r)
    }

    /**
     * Return information about the time zone used by the UNIX time specified by @p epochSecond.
     *
     * @param epochSecond an instant in time expressed as seconds since January 1, 1970 00:00:00 UTC.
     * @return a tuple containing the name of the zone (such as "CET"), the offset in seconds east of UTC,
     * the start and end time when the time zone takes effect.
     *
     * @since 0.19.3
     */
    func lookup(epochSecond: Int64): (String, Int64, Int64, Int64) {
        match (cache) {
            case Some(v) =>
                let (start, end, zone) = v
                if (start <= epochSecond && epochSecond < end && (end != Int64.Max || footer == "")) {
                    return (zone.des, Int64(zone.offset), start, end)
                }
            case _ => ()
        }

        if (transTimes.size == 0 || epochSecond < transTimes[0].trans) {
            var zone = localTimes[inferZoneIndex()]
            var end = Int64.Max
            if (transTimes.size > 0) {
                end = transTimes[0].trans
            }
            return (zone.des, Int64(zone.offset), Int64.Min, end)
        }

        let (low, end) = binarySearch(epochSecond)
        var zone = localTimes[Int64(transTimes[low].index)]
        var name = zone.des
        var offset = zone.offset
        var start = transTimes[low].trans

        /* If no results are found from the known transition time, try TZif footer. */
        try {
            if (low == transTimes.size - 1 && footer != "") {
                return parseTZ(footer, end, epochSecond)
            }
        } catch (_) {
            return (name, Int64(offset), start, end)
        }
        return (name, Int64(offset), start, end)
    }

    /**
     * Search for the corresponding transTime based on epochSecond.
     *
     * @param epochSecond an instant in time expressed as seconds since January 1, 1970 00:00:00 UTC.
     * @return the index in transTimes and the corresponding transTime.
     *
     * @since 0.38.2
     */
    private func binarySearch(epochSecond: Int64): (Int64, Int64) {
        var end = Int64.Max
        var low = 0
        var high = transTimes.size
        while (high - low > 1) {
            var mid = (low + high) / 2
            var curTime = transTimes[mid].trans
            if (epochSecond < curTime) {
                end = curTime
                high = mid
            } else {
                low = mid
            }
        }
        return (low, end)
    }

    /**
     * Return the time zone index used before or without the first transition time.
     *
     * @return the index of the time zone to use for times before or without the first transition time.
     *
     * @since 0.19.3
     */
    func inferZoneIndex(): Int64 {
        /* If zone 0 is unused in transitions, it's the zone to use for early times. */
        var flag = false
        for (tran in transTimes) {
            if (tran.index == 0) {
                flag = true
                break
            }
        }
        if (!flag) {
            return 0
        }

        /*
         * Absent the above, if there are transition times and the first transition is to a daylight time,
         * find the standard type less than and closest to the zone of the first transition.
         */
        if (transTimes.size > 0 && localTimes[Int64(transTimes[0].index)].isDST) {
            var index = Int64(transTimes[0].index) - 1
            for (zoneIndex in index..=0 : -1) {
                if (!localTimes[zoneIndex].isDST) {
                    return zoneIndex
                }
            }
        }

        /* If no result yet, find the first standard type. */
        for (zoneIndex in 0..localTimes.size) {
            if (!localTimes[zoneIndex].isDST) {
                return zoneIndex
            }
        }

        /* If there is none, punt to zone zero. */
        return 0
    }

    /**
     * Return the offset of the time zone for a given name and unix time.
     * (The unix time is in the UTC time zone).
     *
     * @param name zone name
     * @param unix unix time in second.
     * @return the zone offset.
     *
     * @since 0.19.3
     */
    func lookupName(name: String, unix: Int64): Int64 {
        var offset = Int64.Min
        for (zone in localTimes) {
            if (zone.des == name) {
                if (offset == Int64.Min) {
                    offset = Int64(zone.offset)
                }
                let (des, off, _, _) = lookup(unix - Int64(zone.offset))
                if (des == zone.des) {
                    return off
                }
            }
        }

        if (offset != Int64.Min) {
            return offset
        }

        return MAX_OFFSET + 1
    }
}

/**
 * Given a POSIX section time zone string, the end unix time of the last time zone transition, and a time,
 * return values are similar to that of lookup.
 *
 * @param footer a POSIX section time zone string.
 * @param end the end of the last time zone transition expressed as seconds since 1970.1.1 00:00:00 UTC.
 * @param epochSecond a time expressed the same way as @p end.
 * @return values are similar to that of lookup.
 *
 * @since 0.19.3
 */
func parseTZ(footer: String, end: Int64, epochSecond: Int64): (String, Int64, Int64, Int64) {
    var stdName: String = ""
    var dstName: String = ""
    var stdOffset = 0
    var dstOffset = 0
    var rest = footer
    (stdName, rest) = parseTZName(rest)
    (stdOffset, rest) = parseTZOffset(rest)
    stdOffset = -stdOffset
    if (rest.size == 0) {
        return (stdName, stdOffset, end, Int64.Max)
    }

    (dstName, rest) = parseTZName(rest)
    if (rest.size == 0 || rest[0] == b',') {
        dstOffset = stdOffset + SECS_PER_HOUR
    } else {
        (dstOffset, rest) = parseTZOffset(rest)
        dstOffset = -dstOffset
    }

    if (rest.size == 0) {
        rest = ",M3.2.0,M11.1.0"
    }
    if (rest[0] != b',') {
        throw InvalidDataException("Failed to parse the timezone data.")
    }

    var startRule: ZoneRule = ZoneRule()
    var endRule: ZoneRule = ZoneRule()
    (startRule, rest) = parseTZRule(rest[1..])
    if (rest.size == 0 || rest[0] != b',') {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    (endRule, rest) = parseTZRule(rest[1..])
    if (rest.size > 0) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }

    var (year, ysec) = toYearAndSecond(epochSecond + SECS_OF_UNIX_TO_AD1)

    /* Calculate the start unix second of current year. */
    let unixSec = daysSinceUnix(year) * SECS_PER_DAY
    if (year <= 0) {
        if (isLeapYear(year)) {
            return (stdName, stdOffset, unixSec, unixSec + SECS_OF_LEAP_YEAR)
        }
        return (stdName, stdOffset, unixSec, unixSec + SECS_OF_NORMAL_YEAR)
    }

    var startSec = transToSecOfYear(year, startRule, stdOffset)
    var endSec = transToSecOfYear(year, endRule, dstOffset)
    if (endSec < startSec) {
        (startSec, endSec) = swap(startSec, endSec)
        (stdName, dstName) = swap(stdName, dstName)
        (stdOffset, dstOffset) = swap(stdOffset, dstOffset)
    }

    if (Int64(ysec) < startSec) {
        return (stdName, stdOffset, unixSec, startSec + unixSec)
    } else if (Int64(ysec) >= endSec) {
        if (isLeapYear(year)) {
            return (stdName, stdOffset, endSec + unixSec, unixSec + SECS_OF_LEAP_YEAR)
        }
        return (stdName, stdOffset, endSec + unixSec, unixSec + SECS_OF_NORMAL_YEAR)
    } else {
        return (dstName, dstOffset, startSec + unixSec, endSec + unixSec)
    }
}

/**
 * Return the swapped value.
 */
func swap<T>(left: T, right: T): (T, T) {
    return (right, left)
}

/**
 * Return the time zone name at the start of the TZif footer string @p footer.
 *
 * @param footer TZif footer string.
 * @return the time zone name, the remainder of @p footer.
 *
 * @since 0.19.3
 */
func parseTZName(footer: String): (String, String) {
    // The size of the footer is determined to be greater than zero
    let strLen = footer.size
    if (footer[0] == b'<') {
        for (i in 0..strLen) {
            if (footer[i] == b'>') {
                return (footer[..(i + 1)], footer[(i + 1)..])
            }
        }
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    for (i in 0..strLen) {
        let c = footer[i]
        if ((c >= b'0' && c <= b'9') || c == b',' || c == b'-' || c == b'+') {
            if (i <= 2) {
                throw InvalidDataException("Failed to parse the timezone data.")
            }
            return (footer[..i], footer[i..])
        }
    }
    if (strLen <= 2) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    return (footer, "")
}

/**
 * Return the time zone offset at the start of the TZif footer string @p footer as a number of seconds.
 *
 * @param footer TZif footer string.
 * @return the time zone offset, the remainder of @p footer.
 *
 * @since 0.19.3
 */
func parseTZOffset(footer: String): (Int64, String) {
    if (footer.size == 0) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    var hours = 0
    var mins = 0
    var secs = 0
    var neg = false
    var rest = footer
    if (footer[0] == b'+') {
        rest = footer[1..]
    } else if (footer[0] == b'-') {
        rest = footer[1..]
        neg = true
    }
    (hours, rest) = getNum(rest)
    if (hours < 0 || hours > 24) { // The number of hours of the day is within the range of 0-24.
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    var off = hours * SECS_PER_HOUR
    if (rest.size == 0 || rest[0] != b':') {
        if (neg) {
            off = -off
        }
        return (off, rest)
    }

    (mins, rest) = getNum(rest[1..])
    if (mins < 0 || mins > 59) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    off += mins * SECS_PER_MINUTE
    if (rest.size == 0 || rest[0] != b':') {
        if (neg) {
            off = -off
        }
        return (off, rest)
    }

    (secs, rest) = getNum(rest[1..])
    if (secs < 0 || secs > 59) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    off += secs
    if (neg) {
        off = -off
    }
    return (off, rest)
}

/**
 * Parses a rule from a TZif footer string @p footer.
 *
 * @param footer TZif footer string.
 * @return the rule, and the remainder of the string.
 *
 * @since 0.19.3
 */
func parseTZRule(footer: String): (ZoneRule, String) {
    var rule = ZoneRule()
    if (footer.size == 0) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    var restValue = rule.parse(footer)
    if (restValue.size == 0 || restValue[0] != b'/') {
        rule.rTime = 2 * SECS_PER_HOUR
        return (rule, restValue)
    }

    let (offset, rest) = parseTZOffset(restValue[1..])
    if (offset < 0) {
        throw InvalidDataException("Failed to parse the timezone data.")
    }
    rule.rTime = offset

    return (rule, rest)
}

/**
 * Parses a number from a TZif footer string @p footer.
 *
 * @param footer TZif footer string.
 * @return the number, the remainder of the string.
 *
 * @since 0.19.3
 */
func getNum(footer: String): (Int64, String) {
    let strLen = footer.size
    if (strLen == 0) {
        throw InvalidDataException("Failed to parse the timezone data")
    }
    var end = strLen
    for (i in 0..strLen) {
        let c = footer[i]
        if (c < b'0' || c > b'9') {
            if (i == 0) {
                throw InvalidDataException("Failed to parse the timezone data")
            }
            end = i
            break
        }
    }
    try {
        if (end == footer.size) {
            return (Int64.parse(footer), "")
        }
        return (Int64.parse(footer[..end]), footer[end..])
    } catch (_: IllegalArgumentException) {
        throw InvalidDataException("Failed to parse the timezone data")
    }
}

/**
 * Return the number of seconds since the start of the @p year that the @p rule takes effect.
 *
 * @param year year.
 * @param rule a zone rule.
 * @param off time zone offset.
 * @return the transtion time in seconds.
 *
 * @since 0.19.3
 */
func transToSecOfYear(year: Int64, rule: ZoneRule, off: Int64): Int64 {
    if (year <= 0) {
        throw InvalidDataException("Failed to parse the timezone data")
    }
    var sec: Int64
    match (rule.rType) {
        case JulianDay =>
            sec = (rule.rDay - 1) * SECS_PER_DAY
            if (isLeapYear(year) && rule.rDay >= 60) { // If the 60day of a leap year is crossed, the number of days must be increased by 1.
                sec += SECS_PER_DAY
            }
        case DayOfYear => sec = rule.rDay * SECS_PER_DAY
        case MonthNthDayOfWeek =>
            let mon = rule.rMon
            let week = rule.rWeek
            let day = rule.rDay

            var dayOfMonBefore = DAYS_BEFORE[rule.rMon - 1]
            if (isLeapYear(year) && mon > 2) { // If the 2th month of a leap year is crossed, the number of days must be increased by 1.
                dayOfMonBefore++
            }
            var dayOfYear = dayOfMonBefore

            // Use the Zeiller formula variant to calculate the day of the week the first day of the month.
            let firstDayOfMon = ((year - 1) + ((year - 1) / 4) - ((year - 1) / 100) + ((year - 1) / 400) +
                dayOfMonBefore + 1) % DAYS_PER_WEEK

            // Days in month from January to December
            var daysInMon = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
            if (isLeapYear(year)) {
                daysInMon[1] = 29 // The number of days in February is 29 in leap years.
            }
            var dayOfMon = 0
            if (firstDayOfMon == 0) {
                dayOfMon += DAYS_PER_WEEK * (week - 1) + day
            } else if (day < firstDayOfMon) {
                dayOfMon += DAYS_PER_WEEK * (week - 1) + day + (DAYS_PER_WEEK - firstDayOfMon)
            } else {
                dayOfMon += DAYS_PER_WEEK * (week - 1) + day - firstDayOfMon
            }
            if ((dayOfMon + 1) > daysInMon[mon - 1]) {
                dayOfMon -= DAYS_PER_WEEK
            }
            dayOfYear += dayOfMon
            sec = dayOfYear * SECS_PER_DAY
    }

    return sec + rule.rTime - off
}

public class InvalidDataException <: Exception {
    public init() {
        super()
    }

    public init(message: String) {
        super(message)
    }

    protected override func getClassName(): String {
        return "InvalidDataException"
    }
}