/**
 * mobile.js - Base mobile application logic.
 *
 * Copyright 2010-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @author   Michael J. Rubinsky <mrubinsk@horde.org>
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/gpl GPL
 * @package  Kronolith
 */
var KronolithMobile = {

    /**
     * List of calendars we are displaying
     */
    calendars:  [],

    /**
     * List of calendars that are currently loaded for the current view
     */
    loadedCalendars: [],

    /**
     * Event cache
     */
    ecache: {},
    cacheStart: null,
    cacheEnd: null,

    deferHash: {},
    viewRan: false,

    /**
     * The currently displayed view
     */
    view: 'day',

    /**
     * The currently selected date
     */
    date: null,

    /**
     * Temporary fix for pages not firing pagebeforecreate events properly
     */
    haveOverview: false,

    /**
     * Load all events between start and end time.
     *
     * @param Date firstDay
     * @param Date lastDay
     * @param string view    The view we are loading for (month, day)
     */
    loadEvents: function(firstDay, lastDay, view)
    {
        var dates = [firstDay, lastDay], loading = false;

        // Clear out the loaded cal cache
        KronolithMobile.loadedCalendars = [];
        KronolithMobile.clearView(view);
        KronolithMobile.viewRan = false;
        $.each(KronolithMobile.calendars, function(key, cal) {
            var startDay = dates[0].clone(), endDay = dates[1].clone(),
            cals = KronolithMobile.ecache[cal[0]], c;
            if (typeof cals != 'undefined' &&
                typeof cals[cal[1]] != 'undefined') {

                cals = cals[cal[1]];
                c = cals[startDay.dateString()];
                while (typeof c != 'undefined' && startDay.isBefore(endDay)) {
                    if (view == 'day') {
                        KronolithMobile.insertEvents([startDay, startDay], view, cal.join('|'));
                    }
                    startDay.addDays(1);
                    c = cals[startDay.dateString()];
                }

                c = cals[endDay.dateString()];
                while (typeof c != 'undefined' && !startDay.isAfter(endDay)) {
                    if (view == 'day') {
                        KronolithMobile.insertEvents([endDay, endDay], view, cal.join('|'));
                    }
                    endDay.addDays(-1);
                    c = cals[endDay.dateString()];
                }
                if (startDay.compareTo(endDay) > 0) {
                    KronolithMobile.loadedCalendars.push(cal.join('|'));
                    return;
                }
            }

            var start = startDay.dateString(), end = endDay.dateString();
                loading = true;
            HordeMobile.doAction('listEvents',
                                 {
                                   'start': start,
                                   'end': end,
                                   'cal': cal.join('|'),
                                   'view': view,
                                   'sig': start + end + (Math.random() + '').slice(2)
                                 },
                                 KronolithMobile.loadEventsCallback
            );
        });

        if (!loading && view == 'overview') {
            KronolithMobile.insertEvents([firstDay, lastDay], view);
        }
    },

    /**
     * Sort a collection of events as returned from the ecache
     */
    sortEvents: function(events)
    {
        var e = [];

        // Need a native array to sort.
        $.each(events, function(id, event) {
            e.push(event);
        });
        return  e.sort(function(a, b) {
           sortA = a.sort;
           sortB = b.sort;
           return (sortA < sortB) ? -1 : (sortA > sortB) ? 1 : 0;
         });
    },

    /**
     * Callback for the loadEvents AJAX request.
     *
     * @param object data  The ajax response.
     */
    loadEventsCallback: function(data)
    {
        var start = KronolithMobile.parseDate(data.sig.substr(0, 8)),
            end = KronolithMobile.parseDate(data.sig.substr(8, 8)),
            dates = [start, end], view = data.view;

        KronolithMobile.storeCache(data.events, data.cal, dates, true);
        KronolithMobile.loadedCalendars.push(data.cal);
        KronolithMobile.insertEvents(dates, view, data.cal);
    },

    /**
     * Inserts events into current view.
     * For Day view, builds a new listview and attaches to the DOM.
     * For Month view, hightlights dates with events.
     */
    insertEvents: function(dates, view, cal)
    {
        var key = dates[0].dateString() + dates[1].dateString() + view + cal,
        d = [dates[0].clone(), dates[1].clone()], date, events, list, day;

        // Make sure all calendars are loaded before rendering the view.
        // @TODO: Implement LIFO queue as in kronolith.js
        if (KronolithMobile.loadedCalendars.length != KronolithMobile.calendars.length) {
            if (KronolithMobile.deferHash[key]) {
                return;
            } else {
                KronolithMobile.deferHash[key] = window.setTimeout(function() { KronolithMobile.insertEvents(d, view, cal); }, 0);
                return;
            }
        }
        if (KronolithMobile.deferHash[key]) {
            window.clearTimeout(KronolithMobile.deferHash[key]);
            KronolithMobile.deferHash[key] = false;
        }

        KronolithMobile.running = true;
        switch (view) {
            case 'day':
                if (!KronolithMobile.viewRan) {
                    KronolithMobile.viewRan = true;
                    date = d[0].dateString();
                    events = KronolithMobile.getCacheForDate(date);
                    events = KronolithMobile.sortEvents(events);
                    list = $('<ul>').attr({'data-role': 'listview'});
                    $.each(events, function(index, event) {
                        list.append(KronolithMobile.buildDayEvent(event));
                    });
                    if (!list.children().length) {
                        list.append($('<li>').text(Kronolith.text.noevents));
                    }
                    $("#dayview :jqmData(role='content')").append(list).trigger('create');
                }
                break;

            case 'month':
                day = d[0].clone();
                while (!day.isAfter(d[1])) {
                    date = day.dateString();
                    events = KronolithMobile.getCacheForDate(date);
                    $.each(events, function(key, event) {
                        $('#kronolithMonth' + date).addClass('kronolithContainsEvents');
                    });
                    day.next().day();
                }
                // Select current date.
                $('#kronolithMonth'+ KronolithMobile.date.dateString()).addClass('kronolith-selected');
                KronolithMobile.selectMonthDay(KronolithMobile.date.dateString());
                break;

            case 'overview':
                day = d[0].clone(), haveEvent = false;
                list = $('<ul>').attr({'data-role': 'listview'});
                while (!day.isAfter(d[1])) {
                    list.append($('<li>').attr({ 'data-role': 'list-divider' }).text(day.toString('ddd') + ' ' + day.toString('d')));
                    events = KronolithMobile.sortEvents(KronolithMobile.getCacheForDate(day.dateString())) ;
                    $.each(events, function(index, event) {
                        list.append(KronolithMobile.buildDayEvent(event));
                        haveEvent = true;
                    });
                    if (!haveEvent) {
                        list.append($('<li>').text(Kronolith.text.noevents));
                    }
                    haveEvent = false;
                    day.next().day();
                }
                $("#overview :jqmData(role='content')").append(list).trigger('create');
                break;
        }
        KronolithMobile.running = false;
    },

    /**
     * Build the dom element for an event to insert into the day view.
     *
     * @param object event  The event object returned from the ajax request.
     */
    buildDayEvent: function(event)
    {
        var id;
        if ($.isEmptyObject(event)) {
          return;
        }

        var cal = event.calendar, type = cal.split('|')[0], c = cal.split('|')[1],
        d = $('<div>'), item = $('<li>'), a;

        // Time
        var timeWrapper = $('<div>').addClass('kronolithTimeWrapper');
        if (event.al) {
            timeWrapper.append(Kronolith.text.allday).html();
        } else {
            var startTime = Date.parse(event.s).toString(Kronolith.conf.time_format);
            var endTime = '- ' + Date.parse(event.e).toString(Kronolith.conf.time_format);
            timeWrapper
              .append($('<div>').addClass('kronolithStartTime').append(startTime))
              .append($('<div>').addClass('kronolithEndTime').append(endTime));
        }

        e = $('<h2>').text(event.t);
        l = $('<p>').addClass('kronolithDayLocation' + Kronolith.conf.calendars[type][c].fg.substring(1)).text(event.l);
        d.append(timeWrapper).append(e).append(l);

        // Add the link to view the event detail.
        a = $('<a>')
            .css({ backgroundColor: Kronolith.conf.calendars[type][c].bg })
            .attr({ href: '#eventview',
                    'class': 'kronolith-event kronolith-event-' + Kronolith.conf.calendars[type][c].fg.substring(1) })
            .click(function(ev) {
                $("#eventview :jqmData(role='content') ul").detach();
                KronolithMobile.loadEvent(cal, event.id, Date.parse(event.e));
            })
            .append(d);

        return item.append(a);
    },

    /**
     * Retrieve a single event from the server and show it.
     *
     * @param string cal  The calendar identifier.
     * @param string id   The event identifier.
     * @param Date   d    The date the event occurs.
     */
    loadEvent: function(cal, id, d)
    {
        HordeMobile.doAction('getEvent',
                             {'cal': cal, 'id': id, 'date': d.toString('yyyyMMdd')},
                             KronolithMobile.loadEventCallback);
    },

    /**
     * Callback for loadEvent call.  Assume we are in Event view for now, build
     * the event view structure and attach to DOM.
     *
     * @param object data  The ajax response.
     */
    loadEventCallback: function(data)
    {
         if (!data.event) {
             // @TODO: Error handling.
             return;
         }

         var event = data.event;
         var ul = KronolithMobile.buildEventView(event);
         $("#eventview :jqmData(role='content')").append(ul).trigger('create');
    },

    /**
     * Build event view DOM structure and return the top event element.
     *
     * @param object e  The event structure returned from the ajax call.
     */
    buildEventView: function(e)
    {
         var list = $('<ul>')
            .addClass('kronolithEventDetail')
            .attr({'data-role': 'listview', 'data-inset': true});

         var loc = false;

         // Title and calendar
         var title = $('<div>').addClass('kronolithEventDetailTitle').append($('<h2>').text(e.t));
         var calendar = $('<p>').addClass('kronolithEventDetailCalendar').text(Kronolith.conf.calendars[e.ty][e.c]['name']);
         list.append($('<li>').append(title).append(calendar));

         // Time
         var item = $('<div>');
         if (e.r) {
             var recurText = Kronolith.text.recur.desc[e.r.t][(e.r.i > 1) ? 1 : 0],
                 date = Date.parse(e.s);
             recurText = recurText.replace('#{interval}', e.r.i);
             switch (e.r.t) {
             case 2:
                 // WEEKLY
                 recurText = recurText.replace('#{weekday}', Kronolith.text.weekday[e.r.d]);
                 break;
             case 3:
                 // MONTHLY_DATE
                 recurText = recurText.replace('#{date}', date.toString('d') + '.');
                 break;
             case 5:
                 // YEARLY_DATE
                 recurText = recurText.replace('#{date}', date.toString('m'));
                 break;
             }
             item.append($('<div>').addClass('kronolithEventDetailRecurring').append(recurText));
             item.append($('<div>').addClass('kronolithEventDetailRecurring').text(Kronolith.text.recur[e.r.t]));
         }
         if (e.al) {
             item.append($('<div>').addClass('kronolithEventDetailAllDay').text(Kronolith.text.allday))
                .append($('<div>').addClass('kronolithEventDetailDate').text(Date.parse(e.s).toString('D')));
         } else {
             item.append($('<div>')
                .append($('<div>').addClass('kronolithEventDetailDate').text(Date.parse(e.s).toString('D'))
                .append($('<div>').addClass('kronolithEventDetailTime').text(Date.parse(e.s).toString(Kronolith.conf.time_format) + ' - ' + Date.parse(e.e).toString(Kronolith.conf.time_format))))
             );
         }
         list.append($('<li>').append(item));

         // Location
         if (e.gl) {
             loc = $('<div>').addClass('kronolithEventDetailLocation')
                .append($('<a>').attr({'data-style': 'b', 'href': 'http://maps.google.com?q=' + encodeURIComponent(e.gl.lat + ',' + e.gl.lon)}).text(e.l));
         } else if (e.l) {
             loc = $('<div>').addClass('kronolithEventDetailLocation')
                .append($('<a>').attr({'href': 'http://maps.google.com?q=' + encodeURIComponent(e.l)}).text(e.l));
         }
         if (loc) {
             list.append($('<li>').append(loc));
         }

         // Description
         if (e.d) {
           list.append($('<li>').append($('<div>').addClass('kronolithEventDetailDesc').text(e.d)));
         }

         // url
         if (e.u) {
           list.append($('<li>').append($('<a>').attr({'rel': 'external', 'href': e.u}).text(e.u)));
         }

         return list;
    },

    clearView: function(view)
    {
        switch (view) {
        case 'month':
            $('#kronolithDayDetail ul').detach();
            break;
        case 'day':
            $("#dayview :jqmData(role='content') ul").detach();
            break;
        case 'overview':
            $("#overview :jqmData(role='content') ul").detach();
        }
    },

    /**
     * Advance the day view by one day
     */
    showNextDay: function()
    {
        KronolithMobile.moveToDay(KronolithMobile.date.clone().addDays(1));
    },

    /**
     * Move the day view back by one day
     */
    showPrevDay: function()
    {
        KronolithMobile.moveToDay(KronolithMobile.date.clone().addDays(-1));
    },

    /**
     * Move the day view to a specific day
     *
     * @param Date date  The date to set the day view to.
     */
    moveToDay: function(date)
    {
        $('#kronolithDayDate').text(date.toString('ddd') + ' ' + date.toString('d'));
        KronolithMobile.date = date.clone();
        KronolithMobile.loadEvents(KronolithMobile.date, KronolithMobile.date, 'day');
    },

    /**
     * Advance the month view ahead one month.
     */
    showPrevMonth: function()
    {
        KronolithMobile.moveToMonth(KronolithMobile.date.clone().addMonths(-1));
    },

    /**
     * Move the month view back one month
     */
    showNextMonth: function()
    {
        KronolithMobile.moveToMonth(KronolithMobile.date.clone().addMonths(1));
    },

    /**
     * Move the month view to the month containing the specified date.
     *
     * @params Date date  The date to move to.
     */
    moveToMonth: function(date)
    {
        var dates = KronolithMobile.viewDates(date, 'month');
        KronolithMobile.date = date;
        KronolithMobile.loadEvents(dates[0], dates[1], 'month');
        KronolithMobile.buildCal(date);
        KronolithMobile.insertEvents(dates, 'month');
    },

    /**
     * Selects a day in the month view, and displays any events it may contain.
     * Also sets the dayview to the same date, so navigating back to it is
     * smooth.
     *
     * @param string date  A date string in the form of yyyyMMdd.
     */
    selectMonthDay: function(date)
    {
        var ul = $('<ul>').attr({ 'data-role': 'listview' }),
        d = KronolithMobile.parseDate(date), today = new Date(), text;
        $('#kronolithDayDetail ul').detach();
        if (today.dateString() == d.dateString()) {
          text = Kronolith.text.today;
        } else if (today.clone().addDays(-1).dateString() == d.dateString()) {
          text = Kronolith.text.yesterday;
        } else if (today.clone().addDays(1).dateString() == d.dateString()) {
          text = Kronolith.text.tomorrow;
        } else {
          text = d.toString('ddd') + ' ' + d.toString('d');
        }
        $('#kronolithDayDetailHeader h3').text(text);
        $('.kronolith-selected').removeClass('kronolith-selected');
        $('#kronolithMonth' + date).addClass('kronolith-selected');
        if ($('#kronolithMonth' + date).hasClass('kronolithContainsEvents')) {
            var events = KronolithMobile.getCacheForDate(date);
            events = KronolithMobile.sortEvents(events);
            $.each(events, function(k, e) {
                ul.append(KronolithMobile.buildDayEvent(e));
            });
        }
        $('#kronolithDayDetail').append(ul).trigger('create');
        KronolithMobile.moveToDay(d);
    },

    /**
     * Calculates first and last days being displayed.
     *
     * @var Date date    The date of the view.
     * @var string view  A view name.
     *
     * @return array  Array with first and last day of the view.
     */
    viewDates: function(date, view)
    {
        var start = date.clone(), end = date.clone();

        switch (view) {
        case 'month':
            start.setDate(1);
            start.moveToBeginOfWeek(Kronolith.conf.week_start);
            end.moveToLastDayOfMonth();
            end.moveToEndOfWeek(Kronolith.conf.week_start);
            break;
        case 'summary':
            end.add(6).days();
            break;
        }

        return [start, end];
    },

    /**
     * Creates the month view calendar.
     *
     * @param Date date        The date to show in the calendar.
     */
    buildCal: function(date)
    {
        var tbody = $('.kronolith-minical table tbody');
        var dates = KronolithMobile.viewDates(date, 'month'), day = dates[0].clone(),
        today = Date.today(), dateString, td, tr, i;

        // Remove old calendar rows.
        tbody.children().remove();

        // Update title
        $('#kronolithMinicalDate').html(date.toString('MMMM yyyy'));

        for (i = 0; i < 42; i++) {
            dateString = day.dateString();

            // Create calendar row .
            if (day.getDay() == Kronolith.conf.week_start) {
                tr = $('<tr>');
                tbody.append(tr);
            }

            // Insert day cell.
            td = $('<td>').attr({ id: 'kronolithMonth' + dateString })
                .addClass('kronolithMonthDay')
                .jqmData('date', dateString);
            if (day.getMonth() != date.getMonth()) {
                td.addClass('kronolithMinicalEmpty');
            }

            // Highlight today.
            if (day.dateString() == today.dateString()) {
                td.addClass('kronolithToday');
            }
            td.html(day.getDate());
            tr.append(td);
            day.next().day();
        }
    },

    /**
     * Parses a date attribute string into a Date object.
     *
     * For other strings use Date.parse().
     *
     * @param string date  A yyyyMMdd date string.
     *
     * @return Date  A date object.
     */
    parseDate: function(date)
    {
        var d = new Date(date.substr(0, 4), date.substr(4, 2) - 1, date.substr(6, 2));
        if (date.length == 12) {
            d.setHours(date.substr(8, 2));
            d.setMinutes(date.substr(10, 2));
        }
        return d;
    },

    storeCache: function(events, calendar, dates, createCache)
    {
        events = events || {};

        //calendar[0] == type, calendar[1] == calendar name
        calendar = calendar.split('|');
        if (!KronolithMobile.ecache[calendar[0]]) {
            if (!createCache) {
                return;
            }
            KronolithMobile.ecache[calendar[0]] = {};
        }
        if (!KronolithMobile.ecache[calendar[0]][calendar[1]]) {
            if (!createCache) {
                return;
            }
            KronolithMobile.ecache[calendar[0]][calendar[1]] = {};
        }
        var calHash = KronolithMobile.ecache[calendar[0]][calendar[1]];

        // Create empty cache entries for all dates.
        if (!!dates) {
            var day = dates[0].clone(), date;
            while (!day.isAfter(dates[1])) {
                date = day.dateString();
                if (!calHash[date]) {
                    if (!createCache) {
                        return;
                    }
                    if (!KronolithMobile.cacheStart || KronolithMobile.cacheStart.isAfter(day)) {
                        KronolithMobile.cacheStart = day.clone();
                    }
                    if (!KronolithMobile.cacheEnd || KronolithMobile.cacheEnd.isBefore(day)) {
                        KronolithMobile.cacheEnd = day.clone();
                    }
                    calHash[date] = {};
                }
                day.add(1).day();
            }
        }

        var cal = calendar.join('|');
        $.each(events, function(key, date) {
            // We might not have a cache for this date if the event lasts
            // longer than the current view
            if (typeof calHash[key] == 'undefined') {
                return;
            }

            // Store useful information in event objects.
            $.each(date, function(k, event) {
                event.calendar = cal;
                event.start = Date.parse(event.s);
                event.end = Date.parse(event.e);
                event.sort = event.start.toString('HHmmss')
                    + (240000 - parseInt(event.end.toString('HHmmss'), 10)).toPaddedString(6);
                event.id = k;
            });

            // Store events in cache.
            $.extend(calHash[key], date);
        });
    },

    /**
     * Return all events for a single day from all displayed calendars merged
     * into a single hash.
     *
     * @param string date  A yyyymmdd date string.
     *
     * @return Hash  An event hash which event ids as keys and event objects as
     *               values.
     */
    getCacheForDate: function(date, calendar)
    {
        if (calendar) {
            var cals = calendar.split('|');
            return KronolithMobile.ecache[cals[0]][cals[1]][date];
        }

        var events = {};
        $.each(KronolithMobile.ecache, function(key, type) {
            $.each(type, function(id, cal) {
                if (!Kronolith.conf.calendars[key][id].show) {
                    return;
                }
                if (typeof cal[date] != 'undefined') {
                    $.extend(events, cal[date]);
                }
           });
        });

        return events;
    },

    /**
     * Handle swipe events for the current view.
     */
    handleSwipe: function(map)
    {
        switch (KronolithMobile.view) {
        case 'day':
            if (map.type == 'swipeleft') {
                KronolithMobile.showNextDay();
            } else {
                KronolithMobile.showPrevDay();
            }
            break;

        case 'month':
            if (map.type == 'swipeleft') {
                KronolithMobile.showNextMonth();
            } else {
                KronolithMobile.showPrevMonth();
            }
        }
    },

    /**
     * Event handler for the pagebeforechange event that implements loading of
     * deep-linked pages.
     *
     * @param object e     Event object.
     * @param object data  Event data.
     */
    toPage: function(e, data)
    {
        switch (data.options.parsedUrl.view) {
        case 'minical-next':
            KronolithMobile.showNextMonth();
            e.preventDefault();
            break;

        case 'minical-prev':
            KronolithMobile.showPrevMonth();
            e.preventDefault();
            break;

        case 'nextday':
            KronolithMobile.showNextDay();
            e.preventDefault();
            break;

        case 'prevday':
            KronolithMobile.showPrevDay();
            e.preventDefault();
            break;
        }
    },

    /**
     */
    loadPage: function()
    {
        switch (HordeMobile.currentPage()) {
        case 'monthview':
            KronolithMobile.view = "month";
            // (re)build the minical only if we need to
            if (!$("#kronolithMinicalDate").jqmData("date") ||
                ($("#kronolithMinicalDate").jqmData("date").toString("M") != KronolithMobile.date.toString("M"))) {
                KronolithMobile.moveToMonth(KronolithMobile.date);
            }
            break;

        case 'overview':
            KronolithMobile.view = "overview";
            if (!KronolithMobile.haveOverview) {
                KronolithMobile.loadEvents(KronolithMobile.date, KronolithMobile.date.clone().addDays(7), "overview");
                KronolithMobile.haveOverview = true;
            }
            break;

        case null:
            break;

        case 'dayview':
        default:
            KronolithMobile.view = "day";
            $("#kronolithDayDate").html(KronolithMobile.date.toString("ddd") + " " + KronolithMobile.date.toString("d"));
            KronolithMobile.loadEvents(KronolithMobile.date, KronolithMobile.date, "day");
            break;
        }
    },

    onDocumentReady: function()
    {
        KronolithMobile.date = new Date();

        // Build list of calendars we want.
        $.each(Kronolith.conf.calendars, function(key, value) {
            $.each(value, function(cal, info) {
                if (info.show) {
                    KronolithMobile.calendars.push([key, cal]);
                }
            });
        });

        // Bind click and swipe events
        $('body').bind('swipeleft', KronolithMobile.handleSwipe)
            .bind('swiperight', KronolithMobile.handleSwipe);
        $(document).bind('pageshow', KronolithMobile.loadPage)
            .bind('pagebeforechange', KronolithMobile.toPage)
            .on("pageshow", "#eventview", function(event, ui) {
                KronolithMobile.view = "event";
            });

        $('#kronolith-minical').on('click', 'td', function(e) {
            KronolithMobile.selectMonthDay($(e.target).jqmData('date'));
        });

        // Load initial view.
        KronolithMobile.loadPage();
    }
};

$(KronolithMobile.onDocumentReady);
