(function (window) {
    'use strict';

    var PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹';

    function intdiv(a, b) {
        return Math.floor(a / b);
    }

    function pad2(n) {
        return n < 10 ? '0' + n : String(n);
    }

    function toPersianDigits(str) {
        if (typeof window.zertiLocalizeDigits === 'function') {
            return window.zertiLocalizeDigits(str);
        }
        return String(str).replace(/[0-9]/g, function (d) {
            return PERSIAN_DIGITS.charAt(parseInt(d, 10));
        });
    }

    function toLatinDigits(str) {
        if (typeof window.zertiToLatinDigits === 'function') {
            return window.zertiToLatinDigits(str);
        }
        return String(str).replace(/[۰-۹]/g, function (d) {
            return String(PERSIAN_DIGITS.indexOf(d));
        });
    }

    /** Same algorithm as zerti_gregorian_to_jalali() in PHP. */
    function gregorianToJalali(gy, gm, gd) {
        var gDaysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        var jDaysInMonth = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];

        gy -= 1600;
        gm -= 1;
        gd -= 1;

        var gDayNo = 365 * gy + intdiv(gy + 3, 4) - intdiv(gy + 99, 100) + intdiv(gy + 399, 400);
        var i;
        for (i = 0; i < gm; i += 1) {
            gDayNo += gDaysInMonth[i];
        }
        if (gm > 1 && ((gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0)) {
            gDayNo += 1;
        }
        gDayNo += gd;

        var jDayNo = gDayNo - 79;
        var jNp = intdiv(jDayNo, 12053);
        jDayNo %= 12053;

        var jy = 979 + 33 * jNp + 4 * intdiv(jDayNo, 1461);
        jDayNo %= 1461;

        if (jDayNo >= 366) {
            jy += intdiv(jDayNo - 1, 365);
            jDayNo = (jDayNo - 1) % 365;
        }

        var jm = 0;
        while (jm < 11 && jDayNo >= jDaysInMonth[jm]) {
            jDayNo -= jDaysInMonth[jm];
            jm += 1;
        }
        var jd = jDayNo + 1;

        return [jy, jm + 1, jd];
    }

    /** Same algorithm as zerti_jalali_to_gregorian() in PHP. */
    function jalaliToGregorian(jy, jm, jd) {
        jy += 1595;
        var days = -355668 + 365 * jy + intdiv(jy, 33) * 8 + intdiv(((jy % 33) + 3), 4) + jd;
        days += jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186;

        var gy = 400 * intdiv(days, 146097);
        days %= 146097;
        if (days > 36524) {
            gy += 100 * intdiv(--days, 36524);
            days %= 36524;
            if (days >= 365) {
                days += 1;
            }
        }
        gy += 4 * intdiv(days, 1461);
        days %= 1461;
        if (days > 365) {
            gy += intdiv(days - 1, 365);
            days = (days - 1) % 365;
        }
        var gd = days + 1;
        var leap = (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0;
        var monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        var gm = 1;
        for (var i = 0; i < monthDays.length; i += 1) {
            if (gd <= monthDays[i]) {
                break;
            }
            gd -= monthDays[i];
            gm += 1;
        }

        return [gy, gm, gd];
    }

    function isJalaliLeap(jy) {
        var r = jy % 33;
        return [1, 5, 9, 13, 17, 22, 26, 30].indexOf(r) !== -1;
    }

    function jalaliMonthLength(jy, jm) {
        if (jm <= 6) {
            return 31;
        }
        if (jm <= 11) {
            return 30;
        }
        return isJalaliLeap(jy) ? 30 : 29;
    }

    function formatJalaliParts(jy, jm, jd, localizeDigits) {
        var out = pad2(jy) + '/' + pad2(jm) + '/' + pad2(jd);
        if (localizeDigits === false) {
            return out;
        }
        if (localizeDigits === true || (typeof window.zertiShouldLocalizeDigits !== 'function' ? true : window.zertiShouldLocalizeDigits())) {
            return toPersianDigits(out);
        }

        return out;
    }

    function parseJalaliString(raw) {
        var latin = toLatinDigits(String(raw || '').trim());
        if (latin === '') {
            return null;
        }

        var gregorian = latin.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
        if (gregorian) {
            return gregorianToJalali(parseInt(gregorian[1], 10), parseInt(gregorian[2], 10), parseInt(gregorian[3], 10));
        }

        var jalali = latin.match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
        if (!jalali) {
            return null;
        }

        return [parseInt(jalali[1], 10), parseInt(jalali[2], 10), parseInt(jalali[3], 10)];
    }

    function gregorianYmdToJalaliDisplay(ymd, persianDigits) {
        var m = toLatinDigits(String(ymd || '')).match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
        if (!m) {
            return '';
        }
        var parts = gregorianToJalali(parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10));
        return formatJalaliParts(parts[0], parts[1], parts[2], persianDigits !== false);
    }

    function jalaliWeekday(jy, jm, jd) {
        var g = jalaliToGregorian(jy, jm, jd);
        return new Date(g[0], g[1] - 1, g[2]).getDay();
    }

    function todayJalaliParts(timeZone) {
        var now = new Date();
        var fmt = new Intl.DateTimeFormat('en-GB', {
            timeZone: timeZone || undefined,
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
        });
        var map = {};
        fmt.formatToParts(now).forEach(function (part) {
            if (part.type !== 'literal') {
                map[part.type] = part.value;
            }
        });
        return gregorianToJalali(parseInt(map.year, 10), parseInt(map.month, 10), parseInt(map.day, 10));
    }

    window.zertiJalaliDate = {
        toPersianDigits: toPersianDigits,
        toLatinDigits: toLatinDigits,
        gregorianToJalali: gregorianToJalali,
        jalaliToGregorian: jalaliToGregorian,
        jalaliMonthLength: jalaliMonthLength,
        formatJalaliParts: formatJalaliParts,
        parseJalaliString: parseJalaliString,
        gregorianYmdToJalaliDisplay: gregorianYmdToJalaliDisplay,
        jalaliWeekday: jalaliWeekday,
        todayJalaliParts: todayJalaliParts,
    };
})(window);
