/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Code which generates event and task (todo) preview tooltips/titletips
 *  when the mouse hovers over either the event list, the task list, or
 *  an event or task box in one of the grid views.
 *
 *   (Portions of this code were previously in calendar.js and unifinder.js,
 *   some of it duplicated.)
 */

/* exported onMouseOverItem, showToolTip, getPreviewForItem,
             getEventStatusString, getToDoStatusString */

/**
 * PUBLIC: This changes the mouseover preview based on the start and end dates
 * of an occurrence of a (one-time or recurring) calEvent or calToDo.
 * Used by all grid views.
 */

Components.utils.import("resource://calendar/modules/calUtils.jsm");

/**
 * PUBLIC: Displays a tooltip with details when hovering over an item in the views
 *
 * @param   {DOMEvent} occurrenceBoxMouseEvent  the triggering event
 * @returns {boolean}                           true, if the tooltip is displayed
 */
function onMouseOverItem(occurrenceBoxMouseEvent) {
    if ("occurrence" in occurrenceBoxMouseEvent.currentTarget) {
        // occurrence of repeating event or todo
        let occurrence = occurrenceBoxMouseEvent.currentTarget.occurrence;
        const toolTip = document.getElementById("itemTooltip");
        return showToolTip(toolTip, occurrence);
    }
    return false;
}

/**
 * PUBLIC: Displays a tooltip for a given item
 *
 * @param  {Node}               aTooltip  the node to hold the tooltip
 * @param  {CalIEvent|calIToDo} aItem     the item to create the tooltip for
 * @returns {boolean}                     true, if the tooltip is displayed
 */
function showToolTip(aToolTip, aItem) {
    if (aItem) {
        let holderBox = getPreviewForItem(aItem);
        if (holderBox) {
            removeChildren(aToolTip);
            aToolTip.appendChild(holderBox);
            return true;
        }
    }
    return false;
}

/**
 * PUBLIC:  Called when a user hovers over a todo element and the text for the
 * mouse over is changed.
 *
 * @param {calIToDo} toDoItem    the item to create the preview for
 * @param {boolean}  aIsTooltip  enabled if used for tooltip composition (default)
 */
function getPreviewForItem(aItem, aIsTooltip=true) {
    if (cal.isEvent(aItem)) {
        return getPreviewForEvent(aItem, aIsTooltip);
    } else if (cal.isToDo(aItem)) {
        return getPreviewForTask(aItem, aIsTooltip);
    } else {
        return null;
    }
}

/**
 * PUBLIC: Returns the string for status (none), Tentative, Confirmed, or
 * Cancelled for a given event
 *
 * @param   {calIEvent} aEvent The event
 * @returns {String}           The string for the status property of the event
 */
function getEventStatusString(aEvent) {
    switch (aEvent.status) {
        // Event status value keywords are specified in RFC2445sec4.8.1.11
        case "TENTATIVE":
            return cal.calGetString("calendar", "statusTentative");
        case "CONFIRMED":
            return cal.calGetString("calendar", "statusConfirmed");
        case "CANCELLED":
            return cal.calGetString("calendar", "eventStatusCancelled");
        default:
            return "";
    }
}

/**
 * PUBLIC: Returns the string for status (none), NeedsAction, InProcess,
 * Cancelled, orCompleted for a given ToDo
 *
 * @param   {calIToDo} aToDo   The ToDo
 * @returns {String}           The string for the status property of the event
 */
function getToDoStatusString(aToDo) {
    switch (aToDo.status) {
        // Todo status keywords are specified in RFC2445sec4.8.1.11
        case "NEEDS-ACTION":
            return cal.calGetString("calendar", "statusNeedsAction");
        case "IN-PROCESS":
            return cal.calGetString("calendar", "statusInProcess");
        case "CANCELLED":
            return cal.calGetString("calendar", "todoStatusCancelled");
        case "COMPLETED":
            return cal.calGetString("calendar", "statusCompleted");
        default:
            return "";
    }
}

/**
 * PRIVATE: Called when a user hovers over a todo element and the text for the
 * mouse overis changed.
 *
 * @param {calIToDo} toDoItem    the item to create the preview for
 * @param {boolean}  aIsTooltip  enabled if used for tooltip composition (default)
 */
function getPreviewForTask(toDoItem, aIsTooltip=true) {
    if (toDoItem) {
        const vbox = document.createElement("vbox");
        vbox.setAttribute("class", "tooltipBox");
        if (aIsTooltip) {
            // tooltip appears above or below pointer, so may have as little as
            // one half the screen height available (avoid top going off screen).
            vbox.maxHeight = Math.floor(screen.height / 2);
        } else {
            vbox.setAttribute("flex", "1");
        }
        boxInitializeHeaderGrid(vbox);

        let hasHeader = false;

        if (toDoItem.title) {
            boxAppendLabeledText(vbox, "tooltipTitle", toDoItem.title);
            hasHeader = true;
        }

        let location = toDoItem.getProperty("LOCATION");
        if (location) {
            boxAppendLabeledText(vbox, "tooltipLocation", location);
            hasHeader = true;
        }

        // First try to get calendar name appearing in tooltip
        if (toDoItem.calendar.name) {
            let calendarNameString = toDoItem.calendar.name;
            boxAppendLabeledText(vbox, "tooltipCalName", calendarNameString);
        }

        if (toDoItem.entryDate && toDoItem.entryDate.isValid) {
            boxAppendLabeledDateTime(vbox, "tooltipStart", toDoItem.entryDate);
            hasHeader = true;
        }

        if (toDoItem.dueDate && toDoItem.dueDate.isValid) {
            boxAppendLabeledDateTime(vbox, "tooltipDue", toDoItem.dueDate);
            hasHeader = true;
        }

        if (toDoItem.priority && toDoItem.priority != 0) {
            let priorityInteger = parseInt(toDoItem.priority, 10);
            let priorityString;

            // These cut-offs should match calendar-event-dialog.js
            if (priorityInteger >= 1 && priorityInteger <= 4) {
                priorityString = cal.calGetString("calendar", "highPriority"); // high priority
            } else if (priorityInteger == 5) {
                priorityString = cal.calGetString("calendar", "normalPriority"); // normal priority
            } else {
                priorityString = cal.calGetString("calendar", "lowPriority"); // low priority
            }
            boxAppendLabeledText(vbox, "tooltipPriority", priorityString);
            hasHeader = true;
        }

        if (toDoItem.status && toDoItem.status != "NONE") {
            let status = getToDoStatusString(toDoItem);
            boxAppendLabeledText(vbox, "tooltipStatus", status);
            hasHeader = true;
        }

        if (toDoItem.status != null && toDoItem.percentComplete != 0 && toDoItem.percentComplete != 100) {
            boxAppendLabeledText(vbox, "tooltipPercent", String(toDoItem.percentComplete) + "%");
            hasHeader = true;
        } else if (toDoItem.percentComplete == 100) {
            if (toDoItem.completedDate == null) {
                boxAppendLabeledText(vbox, "tooltipPercent", "100%");
            } else {
                boxAppendLabeledDateTime(vbox, "tooltipCompleted", toDoItem.completedDate);
            }
            hasHeader = true;
        }

        let description = toDoItem.getProperty("DESCRIPTION");
        if (description) {
            // display wrapped description lines like body of message below headers
            if (hasHeader) {
                boxAppendBodySeparator(vbox);
            }
            boxAppendBody(vbox, description, aIsTooltip);
        }

        return vbox;
    } else {
        return null;
    }
}

/**
 * PRIVATE: Called when mouse moves over a different, or  when mouse moves over
 * event in event list. The instStartDate is date of instance displayed at event
 * box (recurring or multiday events may be displayed by more than one event box
 * for different days), or null if should compute next instance from now.
 *
 * @param {calIEvent} aEvent       the item to create the preview for
 * @param {boolean}   aIsTooltip   enabled if used for tooltip composition (default)
 */
function getPreviewForEvent(aEvent, aIsTooltip=true) {
    let event = aEvent;
    const vbox = document.createElement("vbox");
    vbox.setAttribute("class", "tooltipBox");
    if (aIsTooltip) {
        // tooltip appears above or below pointer, so may have as little as
        // one half the screen height available (avoid top going off screen).
        vbox.maxHeight = Math.floor(screen.height / 2);
    } else {
        vbox.setAttribute("flex", "1");
    }
    boxInitializeHeaderGrid(vbox);

    if (event) {
        if (event.title) {
            boxAppendLabeledText(vbox, "tooltipTitle", aEvent.title);
        }

        let location = event.getProperty("LOCATION");
        if (location) {
            boxAppendLabeledText(vbox, "tooltipLocation", location);
        }
        if (!(event.startDate && event.endDate)) {
            // Event may be recurrent event.   If no displayed instance specified,
            // use next instance, or previous instance if no next instance.
            event = getCurrentNextOrPreviousRecurrence(event);
        }
        boxAppendLabeledDateTimeInterval(vbox, "tooltipDate", event);

        // First try to get calendar name appearing in tooltip
        if (event.calendar.name) {
            let calendarNameString = event.calendar.name;
            boxAppendLabeledText(vbox, "tooltipCalName", calendarNameString);
        }

        if (event.status && event.status != "NONE") {
            let statusString = getEventStatusString(event);
            boxAppendLabeledText(vbox, "tooltipStatus", statusString);
        }

        if (event.organizer && event.getAttendees({}).length > 0) {
            let organizer = event.organizer;
            boxAppendLabeledText(vbox, "tooltipOrganizer", organizer);
        }

        let description = event.getProperty("DESCRIPTION");
        if (description) {
            boxAppendBodySeparator(vbox);
            // display wrapped description lines, like body of message below headers
            boxAppendBody(vbox, description, aIsTooltip);
        }
        return vbox;
    } else {
        return null;
    }
}

/**
 * PRIVATE: Append a separator, a thin space between header and body.
 *
 * @param {Node}  vbox  box to which to append separator.
 */
function boxAppendBodySeparator(vbox) {
    const separator = document.createElement("separator");
    separator.setAttribute("class", "tooltipBodySeparator");
    vbox.appendChild(separator);
}

/**
 * PRIVATE: Append description to box for body text.  Text may contain
 * paragraphs; line indent and line breaks will be preserved by CSS.
 *
 * @param box           box to which to append body
 * @param textString    text of body
 * @param aIsTooltip    true for "tooltip" and false for "conflict-dialog" case
 */
function boxAppendBody(box, textString, aIsTooltip) {
    let type = (aIsTooltip) ? "description": "vbox";
    let textNode = document.createTextNode(textString);
    let xulDescription = document.createElement(type);
    xulDescription.setAttribute("class", "tooltipBody");
    if (!aIsTooltip) {
        xulDescription.setAttribute("flex", "1");
    }
    xulDescription.appendChild(textNode);
    box.appendChild(xulDescription);
}

/**
 * PRIVATE: Use dateFormatter to format date and time,
 * and to header grid append a row containing localized Label: date.
 *
 * @param {Node}         box            The node to add the date label to
 * @param {String}       labelProperty  The label
 * @param {calIDateTime} date           The datetime object to format and add
 */
function boxAppendLabeledDateTime(box, labelProperty, date) {
    date = date.getInTimezone(cal.calendarDefaultTimezone());
    let formattedDateTime = cal.getDateFormatter().formatDateTime(date);
    boxAppendLabeledText(box, labelProperty, formattedDateTime);
}

/**
 * PRIVATE: Use dateFormatter to format date and time interval,
 * and to header grid append a row containing localized Label: interval.
 *
 * @param box               contains header grid.
 * @param labelProperty     name of property for localized field label.
 * @param item              the event or task
 */
function boxAppendLabeledDateTimeInterval(box, labelProperty, item) {
    let dateString = cal.getDateFormatter().formatItemInterval(item);
    boxAppendLabeledText(box, labelProperty, dateString);
}

/**
 * PRIVATE: create empty 2-column grid for header fields, and append it to box.
 *
 * @param  {Node}  box  The node to create a column grid for
 */
function boxInitializeHeaderGrid(box) {
    let grid = document.createElement("grid");
    grid.setAttribute("class", "tooltipHeaderGrid");
    let rows;
    {
        let columns = document.createElement("columns");
        {
            let labelColumn = document.createElement("column");
            labelColumn.setAttribute("class", "tooltipLabelColumn");
            columns.appendChild(labelColumn);
            let valueColumn = document.createElement("column");
            valueColumn.setAttribute("class", "tooltipValueColumn");
            columns.appendChild(valueColumn);
        }
        grid.appendChild(columns);
        rows = document.createElement("rows");
        grid.appendChild(rows);
    }
    box.appendChild(grid);
}

/**
 * PRIVATE: To headers grid, append a row containing Label: value, where label
 * is localized text for labelProperty.
 *
 * @param box               box containing headers grid
 * @param labelProperty     name of property for localized name of header
 * @param textString        value of header field.
 */
function boxAppendLabeledText(box, labelProperty, textString) {
    let labelText = cal.calGetString("calendar", labelProperty);
    let rows = box.getElementsByTagNameNS(box.namespaceURI, "rows")[0];
    let row = document.createElement("row");

    row.appendChild(createTooltipHeaderLabel(labelText));
    row.appendChild(createTooltipHeaderDescription(textString));

    rows.appendChild(row);
}

/**
 * PRIVATE: Creates an element for field label (for header grid)
 *
 * @param   {String} text  The text to display in the node
 * @returns {Node}         The node
 */
function createTooltipHeaderLabel(text) {
    let label = document.createElement("label");
    label.setAttribute("class", "tooltipHeaderLabel");
    label.appendChild(document.createTextNode(text));
    return label;
}

/**
 * PRIVATE: Creates an element for field value (for header grid)
 *
 * @param   {String} text  The text to display in the node
 * @returns {Node}         The node
 */
function createTooltipHeaderDescription(text) {
    let label = document.createElement("description");
    label.setAttribute("class", "tooltipHeaderDescription");
    label.appendChild(document.createTextNode(text));
    return label;
}

/**
 * PRIVATE: If now is during an occurrence, return the occurrence. If now is
 * before an occurrence, return the next occurrence or otherwise the previous
 * occurrence.
 *
 * @param   {calIEvent}  calendarEvent   The text to display in the node
 * @returns {mixed}                      Returns a calIDateTime for the detected
 *                                        occurence or calIEvent, if this is a
 *                                        non-recurring event
 */
function getCurrentNextOrPreviousRecurrence(calendarEvent) {
    if (!calendarEvent.recurrenceInfo) {
        return calendarEvent;
    }

    let dur = calendarEvent.duration.clone();
    dur.isNegative = true;

    // To find current event when now is during event, look for occurrence
    // starting duration ago.
    let probeTime = cal.now();
    probeTime.addDuration(dur);

    let occ = calendarEvent.recurrenceInfo.getNextOccurrence(probeTime);

    if (!occ) {
        let occs = calendarEvent.recurrenceInfo.getOccurrences(calendarEvent.startDate, probeTime, 0, {});
        occ = occs[occs.length - 1];
    }
    return occ;
}
