module.exports = function (options) {
    var undf = undefined;
    var isNullOrEmpty = function (str) {
        return str === null || str === undefined || str === "";
    };
    var div = function (a, b) {
        return ~~(a / b);
    };
    var mod = function (a, b) {
        return a - ~~(a / b) * b;
    };
    var mod2 = function (a, b) {
        return Math.abs(a - (b * Math.floor(a / b)));
    };
    var toInt = function (text) {
        return parseInt(text, 10);
    }
    var getDays = function (date) {
        var dateArray = date.split('/');
        if (dateArray[1] < 8)
            return (dateArray[1] - 1) * 31 + toInt(dateArray[2]);
        return 6 * 31 + (dateArray[1] - 7) * 30 + toInt(dateArray[2]);
    }
    var isLeapYear = function (year) {
        return ((((year + 12) % 33) % 4) === 1);
    };
    var getDiffDays = function (date1, date2) {
        var diffDays = getDays(date2) - getDays(date1);
        var dateArray1 = date1.split('/');
        var dateArray2 = date2.split('/');
        var y1 = dateArray1[0] < dateArray2[0] ? dateArray1[0] : dateArray2[0];
        var y2 = dateArray1[0] < dateArray2[0] ? dateArray2[0] : dateArray1[0];
        for (var y = y1; y < y2; y++)
            if (isLeapYear(y))
                diffDays += (dateArray1[0] < dateArray2[0]) ? 366 : -366;
            else
                diffDays += (dateArray1[0] < dateArray2[0]) ? 365 : -365;
        return toInt(diffDays);
    };

    var compare = function (date1, date2) {
        var d1, d2;
        if (isNullOrEmpty(date1) || isNullOrEmpty(date2))
            return undf;
        if (Object.prototype.toString.call(date1) !== '[object Date]')
            d1 = new Date(date1);
        else
            d1 = date1;
        if (Object.prototype.toString.call(date2) !== '[object Date]')
            d2 = new Date(date2);
        else
            d2 = date2;
        if (d1.getDate() === d2.getDate() &&
            d1.getMonth() === d2.getMonth() &&
            d1.getFullYear() === d2.getFullYear())
            return 0;
        if (d1.getFullYear() > d2.getFullYear())
            return 1;
        if (d1.getFullYear() < d2.getFullYear())
            return 2;
        if (d1.getMonth() > d2.getMonth())
            return 1;
        if (d1.getMonth() < d2.getMonth())
            return 2;
        if (d1.getDate() > d2.getDate())
            return 1;
        if (d1.getDate() < d2.getDate())
            return 2;
    };
    var daysBetween = function (date1, date2) {
        var d1, d2;
        if (isNullOrEmpty(date1) || isNullOrEmpty(date2))
            return undf;
        if (Object.prototype.toString.call(date1) !== '[object Date]')
            d1 = new Date(date1);
        else
            d1 = date1;
        if (Object.prototype.toString.call(date2) !== '[object Date]')
            d2 = new Date(date2);
        else
            d2 = date2;
        if (d1.getDate() === d2.getDate() &&
            d1.getMonth() === d2.getMonth() &&
            d1.getFullYear() === d2.getFullYear())
            return 0;
        var startDate = Date.UTC(d1.getFullYear(), d1.getMonth(), d1.getDate());
        var endDate = Date.UTC(d2.getFullYear(), d2.getMonth(), d2.getDate());
        var timeDiff = (new Date(startDate)) - (new Date(endDate));
        var days = timeDiff / (1000 * 60 * 60 * 24);
        return days;
    };
    var isFirstHalfYear = function (date) {
        var persianDate = toJalaali(date);
        var month = persianDate[1];
        return parseInt(month + 1) >= 1 && parseInt(month + 1) <= 6;
    }
    var monthsBetween = function (firstDate, secondDate) {
        var diff = daysBetween(secondDate, firstDate);
        var isInFirstHalf = isFirstHalfYear(firstDate);
        var monthCount = isInFirstHalf ? diff / 30 : diff / 29;
        return Number(monthCount);
    }
    var getWeekDay = function (date) {
        return mod2(getDiffDays('1392/03/25', date), 7);
    };
    var jalCal = function (jy) {
        var breaks = [
            -61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456,
            3178
        ],
            bl = breaks.length,
            gy = jy + 621,
            leapJ = -14,
            jp = breaks[0],
            jm,
            jump,
            leap,
            n,
            i;

        if (jy < jp || jy >= breaks[bl - 1])
            throw new Error('Invalid Jalaali year ' + jy);

        for (i = 1; i < bl; i += 1) {
            jm = breaks[i];
            jump = jm - jp;
            if (jy < jm)
                break;
            leapJ = leapJ + div(jump, 33) * 8 + div(mod(jump, 33), 4);
            jp = jm;
        }
        n = jy - jp;

        leapJ = leapJ + div(n, 33) * 8 + div(mod(n, 33) + 3, 4);
        if (mod(jump, 33) === 4 && jump - n === 4)
            leapJ += 1;

        var leapG = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150;

        var march = 20 + leapJ - leapG;

        if (jump - n < 6)
            n = n - jump + div(jump + 4, 33) * 33;
        leap = mod(mod(n + 1, 33) - 1, 4);
        if (leap === -1) {
            leap = 4;
        }

        return {
            leap: leap,
            gy: gy,
            march: march
        };
    };
    var g2D = function (gy, gm, gd) {
        var d = div((gy + div(gm - 8, 6) + 100100) * 1461, 4) + div(153 * mod(gm + 9, 12) + 2, 5) + gd - 34840408;
        d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752;
        return d;
    };
    var d2G = function (jdn) {
        var j;
        j = 4 * jdn + 139361631;
        j = j + div(div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908;
        var i = div(mod(j, 1461), 4) * 5 + 308;
        var gd = div(mod(i, 153), 5) + 1;
        var gm = mod(div(i, 153), 12) + 1;
        var gy = div(j, 1461) - 100100 + div(8 - gm, 6);
        return {
            gy: gy,
            gm: gm,
            gd: gd
        };
    };
    var j2D = function (jy, jm, jd) {
        var r = jalCal(jy);
        return g2D(r.gy, 3, r.march) + (jm - 1) * 31 - div(jm, 7) * (jm - 7) + jd - 1;
    };
    var d2J = function (jdn) {
        var gy = d2G(jdn).gy
            ,
            jy = gy - 621,
            r = jalCal(jy),
            jdn1F = g2D(gy, 3, r.march),
            jd,
            jm,
            k;
        k = jdn - jdn1F;
        if (k >= 0) {
            if (k <= 185) {
                jm = 1 + div(k, 31);
                jd = mod(k, 31) + 1;
                return {
                    jy: jy,
                    jm: jm,
                    jd: jd
                };
            } else {
                k -= 186;
            }
        } else {
            jy -= 1;
            k += 179;
            if (r.leap === 1)
                k += 1;
        }
        jm = 7 + div(k, 30);
        jd = mod(k, 30) + 1;
        return {
            jy: jy,
            jm: jm,
            jd: jd
        };
    };
    var getMonthDays = function (date) {
        var dateArray = date.split('/');
        if (dateArray[1] < 7)
            return 31;
        if (dateArray[1] < 12)
            return 30;
        return isLeapYear(dateArray[0]) ? 30 : 29;
    }
    var isLeapJalaaliYear = function (jy) {
        return jalCal(jy).leap === 0;
    };
    var isLeapGregorianYear = function (year) {
        return (year & 3) === 0 && ((year % 25) !== 0 || (year & 15) === 0);
    };
    var jalaaliMonthLength = function (jy, jm) {
        if (jm <= 6) return 31;
        if (jm <= 11) return 30;
        if (isLeapJalaaliYear(jy)) return 30;
        return 29;
    };
    var gregorianMonthLength = function (jy, jm) {
        return new Date(jy, jm, 0).getDate();
    };
    var isValidJalaaliDate = function (jy, jm, jd) {
        return jy >= -61 &&
            jy <= 3177 &&
            jm >= 1 &&
            jm <= 12 &&
            jd >= 1 &&
            jd <= jalaaliMonthLength(jy, jm);
    };
    var isValidGregorianDate = function (gy, gm, gd) {
        return gy >= 1920 &&
            gm >= 1 &&
            gm <= 12 &&
            gd >= 1 &&
            gd <= gregorianMonthLength(gy, gm);
    };
    var toJalaali = function (gy, gm, gd) {
        var date;
        if (isNullOrEmpty(gy))
            return undf;
        else if (Object.prototype.toString.call(gy) === '[object Date]') {
            gd = gy.getDate();
            gm = gy.getMonth() + 1;
            gy = gy.getFullYear();
        } else if (Object.prototype.toString.call(gy) === '[object String]'
            && (gy.indexOf(":") !== -1 || gy.indexOf("-") !== -1)) {
            date = new Date(gy);
            gd = date.getDate();
            gm = date.getMonth() + 1;
            gy = date.getFullYear();
        } else if (Object.prototype.toString.call(gy) === '[object String]'
            && gy.indexOf("/") !== -1) {
            var splits = gy.split("/");
            if (splits.length === 3 && splits[2].length === 4) {
                gd = Number(splits[1]);
                gm = Number(splits[0]);
                gy = Number(splits[2]);
            } else {
                return undf;
            }
        }

        if (isValidGregorianDate(gy, gm, gd)) {
            date = d2J(g2D(gy, gm, gd));
            if (isValidJalaaliDate(date.jy, date.jm, date.jd))
                return date;
        }

        return undf;
    };
    var getCurrentJalaaliYear = function () {
        var jDate = toJalaali(new Date());
        return jDate
    }
    var getCurrentGregorianYear = function () {
        var date = new Date();
        return date.getYear();
    }
    var getActionLogDate = function (startDate, endDate) {
        var date = dateNow("en");
        var now;
        if (isNullOrEmpty(startDate) && isNullOrEmpty(endDate)) {
            endDate = date;
            var persianEndDate = toJalaali(endDate);
            var persianStartDate = formatDate(persianEndDate.jy, persianEndDate.jm, 1, "jyyyy/jmm/jdd");
            var fm = toGregorian(persianStartDate);
            startDate = formatDate(fm.gy, fm.gm, fm.gd, "mm/dd/yyyy");
            return [startDate, endDate];
        }
        if (isNullOrEmpty(startDate) && !isNullOrEmpty(endDate)) {
            startDate = new Date(endDate);
            startDate.setMonth(startDate.getMonth() - 6);
            now = new Date();
            if (endDate > now)
                endDate = now;

            startDate = formatDate(startDate.getFullYear(), startDate.getMonth() + 1, startDate.getDate(), "mm/dd/yyyy");
            return [startDate, endDate];
        }
        if (!isNullOrEmpty(startDate) && isNullOrEmpty(endDate)) {
            endDate = new Date(startDate);
            endDate.setMonth(endDate.getMonth() + 6);
            now = new Date();
            if (endDate > now)
                endDate = now;

            endDate = formatDate(endDate.getFullYear(), endDate.getMonth() + 1, endDate.getDate(), "mm/dd/yyyy");
            return [startDate, endDate];
        }
        if (daysBetween(startDate, endDate) > 0) {
            endDate = new Date(endDate);
            now = new Date();
            if (endDate > now)
                endDate = now;
            startDate = new Date(endDate);
            startDate.setMonth(endDate.getMonth() - 6);
            startDate = formatDate(startDate.getFullYear(), startDate.getMonth() + 1, startDate.getDate(), "mm/dd/yyyy");
            endDate = formatDate(endDate.getFullYear(), endDate.getMonth() + 1, endDate.getDate(), "mm/dd/yyyy");
            return [startDate, endDate];
        }
        if (monthsBetween(startDate, endDate) > 6) {
            endDate = new Date(endDate);
            startDate = new Date(endDate);
            startDate.setMonth(endDate.getMonth() - 6);
            startDate = formatDate(startDate.getFullYear(), startDate.getMonth() + 1, startDate.getDate(), "mm/dd/yyyy");
            endDate = formatDate(endDate.getFullYear(), endDate.getMonth() + 1, endDate.getDate(), "mm/dd/yyyy");
            return [startDate, endDate];
        }
        return [startDate, endDate];
    }
    var toGregorian = function (jd, jm, jy) {
        if (isNullOrEmpty(jd)) {
            return undefined;
        } else if (!jm || !jy) {
            var splits = jd.split("/");
            jd = Number(splits[2]);
            jm = Number(splits[1]);
            jy = Number(splits[0]);
        }
        if (isValidJalaaliDate(jy, jm, jd))
            var date = d2G(j2D(jy, jm, jd));
        if (!isNullOrEmpty(date) && isValidGregorianDate(date.gy, date.gm, date.gd))
            return date;
        return undf;
    };
    var formatDate = function (y, m, d, pattren) {
        switch (pattren) {
            case "jyyyy/jmm/jdd":
                return y + "/" + m + "/" + d;
            case "mm/dd/yyyy":
                return m + "/" + d + "/" + y;
        }
    };
    var formatPersianDate = function (date) {
        var jalali;
        if (ko.isObservable(date)) {
            if (date() !== null && date() !== undefined && date() !== "") {
                jalali = toJalaali(new Date(date()));
                return formatDate(jalali.jy, jalali.jm, jalali.jd, "jyyyy/jmm/jdd");
            } else
                return "--";
        } else {
            if (date !== null && date !== undefined && date !== "") {
                jalali = toJalaali(new Date(date));
                return formatDate(jalali.jy, jalali.jm, jalali.jd, "jyyyy/jmm/jdd");
            } else
                return "--";
        }
    };
    var formatPersianDateTime = function (date) {
        var jalali, formated, newDate;
        if (ko.isObservable(date)) {
            if (date() !== null && date() !== undefined && date() !== "") {
                newDate = new Date(date());
                jalali = toJalaali(newDate);
                formated = formatDate(jalali.jy, jalali.jm, jalali.jd, "jyyyy/jmm/jdd");
                return formated + " " + newDate.getHours() + ":" + newDate.getMinutes() + ":" + newDate.getSeconds();
            } else
                return "--";
        } else {
            if (date !== null && date !== undefined && date !== "") {
                newDate = new Date(date);
                jalali = toJalaali(newDate);
                formated = formatDate(jalali.jy, jalali.jm, jalali.jd, "jyyyy/jmm/jdd");
                return formated + " " + newDate.getHours() + ":" + newDate.getMinutes() + ":" + newDate.getSeconds();
            } else
                return "--";
        }
    };
    var spliteDateStr = function (str) {
        var d, m, y, date;
        try {
            if (str.indexOf(":") !== -1 || str.indexOf("-") !== -1) {
                date = new Date(str);
                d = date.getDate();
                m = date.getMonth() + 1;
                y = date.getFullYear();
                return {
                    y: y,
                    m: m,
                    d: d,
                    type: "g"
                };
            }
            else if (str.indexOf("/") !== -1) {
                var splits = str.split("/");
                if (splits.length === 3 && splits[2].length === 4) {
                    d = Number(splits[1]);
                    m = Number(splits[0]);
                    y = Number(splits[2]);
                    return {
                        y: y,
                        m: m,
                        d: d,
                        type: "g"
                    };
                }
                if (splits.length === 3 && splits[0].length === 4) {
                    d = Number(splits[2]);
                    m = Number(splits[1]);
                    y = Number(splits[0]);
                    return {
                        y: y,
                        m: m,
                        d: d,
                        type: "j"
                    };
                }

            }
            return undf;
        } catch (e) {
            return undf;
        }
    };
    var fromDatePicker = function (newValue, dateType) {
        var date = spliteDateStr(newValue);
        if (!date)
            return undf;
        switch (dateType) {
            case "fa":
                if (date.type === "j") {
                    var gDate = toGregorian(date.d, date.m, date.y);
                    if (gDate)
                        return formatDate(gDate.gy, gDate.gm, gDate.gd, "mm/dd/yyyy");
                    return undf;
                }
                break;
        }
        return undf;
    };
    var toDatePicker = function (str) {
        var data = spliteDateStr(str);
        if (data.type === "j") {
            return [data, formatDate(data.y, data.m, data.d, "jyyyy/jmm/jdd")];
        }
        if (data.type === "g") {
            var jalaliDate = toJalaali(data.y, data.m, data.d);
            return [jalaliDate, formatDate(jalaliDate.jy, jalaliDate.jm, jalaliDate.jd, "jyyyy/jmm/jdd")];
        }
    };
    var dateNow = function (dateType) {
        var dateStr = localStorage.getItem("ServerDate");
        var date;
        if (!isNullOrEmpty(dateStr) && dateStr !== "")
            date = new Date(dateStr);
        else
            date = new Date();
        var day = date.getDate();
        var month = date.getMonth() + 1;
        var year = date.getFullYear();
        switch (dateType) {
            case "fa":
                var jalaliDate = toJalaali(year, month, day);
                return formatDate(jalaliDate.jy, jalaliDate.jm, jalaliDate.jd, "jyyyy/jmm/jdd");
            case undefined:
            case "":
            case "en":
                return formatDate(year, month, day, "mm/dd/yyyy");
        }
    };
    var getDateLastDayOfYear = function () {
        var endDate;
        var jalali = toJalaali(new Date());
        var formated = formatDate(jalali.jy, jalali.jm, jalali.jd, "jyyyy/jmm/jdd");
        var year = formated.substring(0, 4);
        var isLeap = isLeapJalaaliYear(year);
        if (isLeap)
            endDate = (year + "/12/30");
        else
            endDate = (year + "/12/29");
        var m = toGregorian(endDate);
        return formatDate(m.gy, m.gm, m.gd, "mm/dd/yyyy");
    };
    return {
        formatPersianDate: formatPersianDate,
        formatPersianDateTime: formatPersianDateTime,
        getDateLastDayOfYear: getDateLastDayOfYear,
        getCurrentGregorianYear: getCurrentGregorianYear,
        daysBetween: daysBetween,
        compare: compare,
        getMonthDays: getMonthDays,
        dateNow: dateNow,
        getWeekDay: getWeekDay,
        toDatePicker: toDatePicker,
        fromDatePicker: fromDatePicker,
        spliteDateStr: spliteDateStr,
        toJalaali: toJalaali,
        toGregorian: toGregorian,
        isLeapGregorianYear: isLeapGregorianYear,
        isValidJalaaliDate: isValidJalaaliDate,
        isLeapJalaaliYear: isLeapJalaaliYear,
        getCurrentJalaaliYear: getCurrentJalaaliYear,
        getActionLogDate: getActionLogDate,
        mod: mod,
        g2d: g2D,
        d2j: d2J,
        j2d: j2D,
        jalCal: jalCal
    };
}();
