// The size of each line in a menu.
var lineHeight = 20;

// A global timer to update the display and draw the menu opening and
// closing animation.
var timer = 0;

// An array of animations so that I can handle as much animation as the
// user can start.
var animations = new Array();

// Just a little hack to return data from a synchronous Ajax method call.
var clickthroughResult;

// Get the height of a menu.
// TODO: Fix this code.
function getNodeSize(content)
{
    var size = 0;

    // Add up the heights of all the children.
    for(var i = 0; i < content.childNodes.length; ++i)
    {
        // Get the total height of a child.
        var elementSize = getNodeSize(content.childNodes[i]);

        // If the child is a list element, and has children of its own,
        // reduce the total height by one line, otherwise, set the child's
        // height to one line.
        if(content.childNodes[i].nodeName == "LI")
            if(elementSize)
                elementSize -= lineHeight;
            else
                elementSize = lineHeight;

        size += elementSize;
    }

    return size;
}

// Get the height of a menu.
function getMenuSize(menu)
{
    var size = getNodeSize(menu);

    return size;
}

// Create an Animation object.
// Parameters:
//   element   - The user interface element that is animating.
//   start     - The starting value for the animation.
//   end       - The ending value for the animation.
//   increment - The positive or negative increment for each animation step.
//   method    - The method to execute at each animation step.
function Animation(element, start, end, increment, method)
{
    this.element = element;

    // The animation value can change in either direction. Make sure
    // to use absolute value for the size.
    this.size = Math.abs(end - start);

    // The counter is used to divide the animation up into discrete steps.
    // It always counts down from a positive value to zero.
    this.counter = Math.abs(Math.round(this.size / increment));

    // The values are used in the animation method.
    this.value = start;
    this.increment = increment;

    // Use a user-defined method to perform the animation.
    this.animate = method;

    // Start the animation.
    this.startAnimation();
}

// Define object methods.
Animation.prototype.startAnimation = startAnimation;

// Routine to start the animations.
function startAnimation()
{
    // Add the new animation to the list.
    animations.push(this);

    // If I don't already have a timer, start one.
    if(!timer)
        timer = setInterval("runAnimations()", 20);
}

// Create a Menu Animation object.
// Parameters:
//   menu      - The menu to animate.
//   indicator - The menu disclosure indicator image.
function MenuAnimation(menu, indicator)
{
    // Get the size of the menu and calculate the increment to animate the
    // menu in 5 steps.
    var size = getMenuSize(menu);
    var increment = Math.round(size / 5);

    this.menu = menu;
    this.indicator = indicator;

    // Connect the fade method.
    this.fade = fade;

    // If the menu is currently hidden, show it on the animation.
    // Also set the target indicator type.
    if(indicator.alt == 'closed')
    {
        this.indicator_alt = 'open';

        Animation.call(this, menu, 0, size, increment, scrollMenu);
    }
    // Otherwise, hide it.
    else
    {
        this.indicator_alt = 'closed';

        Animation.call(this, menu, size, 0, -increment, scrollMenu);
    }
}

// Define the animation start routine.
MenuAnimation.prototype.startAnimation = startAnimation;

// Create a new FadeIn Animation object.
function FadeInAnimation(element)
{
    Animation.call(this, element, 0, 5, 1, fade);
}

// Define the animation start routine.
FadeInAnimation.prototype.startAnimation = startAnimation;

// Create a new FadeOut Animation object.
function FadeOutAnimation(element)
{
    Animation.call(this, element, 5, 0, -1, fade);
}

// Define the animation start routine.
FadeOutAnimation.prototype.startAnimation = startAnimation;

// Create a new Dim Animation object.
function DimAnimation(element, startOpacity, endOpacity)
{
    Animation.call(
        this, 
        element, 
        startOpacity, 
        endOpacity, 
        Math.round((endOpacity - startOpacity)/ 5), 
        dim);
}

// Define the animation start routine.
DimAnimation.prototype.startAnimation = startAnimation;

// Timer callback to run animiations.
function runAnimations()
{
    // Create a new animations array to hold the next round of animations.
    var newAnimations = new Array();

    // Run one iteration of each animation.
    for(var i = 0; i < animations.length; ++i)
    {
        var animation = animations[i];

        animation.value += animation.increment;

        // Perform the requested animation.
        animation.animate();

        // Decrement the animation counter and only add it to the new list if
        // there are iterations left.
        // If an animation size happens to be zero (an empty menu, for example)
        // the counter can start at zero and go negative.
        if(--animation.counter > 0)
            newAnimations.push(animation);
    }

    // Reset the animations.
    animations = newAnimations;

    // If my animation list is empty, reset the timer.
    if(!animations.length)
    {
        clearInterval(timer);
        timer = 0;
    }
}

// Timer callback to toggle a menu.
function scrollMenu()
{
    // Make sure the value stays valid
    if(this.value >= this.size)
        this.value = this.size;
    else if(this.value <= 0)
        this.value = 0;

    // If the indicator is wrong, correct it.
    if(this.indicator_alt != this.indicator.alt)
    {
        this.indicator.alt = this.indicator_alt;

        // Adjust the image to match the direction.
        if(this.indicator.alt == 'open')
            this.indicator.src = 'images/tridown.gif';
        else
            this.indicator.src = 'images/tri.gif';
    }

    this.element.style.height = this.value + 'px';

    this.fade();
}

// Open or close a menu.
function toggleMenu(id, img_id)
{
    new MenuAnimation(
        document.getElementById(id), document.getElementById(img_id));
}

// Timer callback to fade an element.
function fade()
{
    this.element.style.opacity = this.value / this.size;
    // IE hacks. 
    this.element.style.zoom = 1;
    this.element.style.filter = 
        "alpha(opacity=" 
        + Math.round(this.element.style.opacity * 100) 
        + ")";
}

// Fade in an element.
function fadeIn(element)
{
    new FadeInAnimation(element);
}

// Fade out an element.
function fadeOut(element)
{
    new FadeOutAnimation(element);
}

// Timer animation to dim an element.
function dim()
{
    // Set the opacity.
    this.element.style.opacity = this.value / 100;

    // Set the z-index as well. Subtract one since z-index can be negative that that
    // will allow an element being faded to 0 opacity to get a z-index of -1 and be 
    // behind other elements.
    this.element.style.zIndex = this.value - 1;

    // IE hacks. 
    this.element.style.zoom = 1;
    this.element.style.filter = 
        "alpha(opacity=" 
        + Math.round(this.element.style.opacity * 100) 
        + ")";
}

// Dim an element.
function dimAnimation(element, startOpacity, endOpacity)
{
    new DimAnimation(element, startOpacity, endOpacity);
}

// Show the editing frame.
function showEditArea(id)
{
    fadeIn(document.getElementById(id));
}

// Hide the editing frame.
function hideEditArea(id)
{
    fadeOut(document.getElementById(id));
}

// Set the edit dialog type.
function setEditDialogType(type)
{
    // Turn on the radio button appropriate to the type.
    document.edit_dialog.textType.checked = (type == 'text');
    document.edit_dialog.linkType.checked = (type == 'link');
    document.edit_dialog.menuType.checked = (type == 'menu');
    document.edit_dialog.categoryType.checked = (type == 'category');

    // Disable the link field if the type isn't a link.
    document.edit_dialog.url.disabled = (type != 'link');

    // Turn off children button by default.
    document.edit_dialog.can_have_children.checked = false;
    document.edit_dialog.can_have_children.disabled = false;

    // Turn off clickthrough selection by default.
    document.edit_dialog.clickthrough_select.selectedIndex = -1;
    document.edit_dialog.clickthrough_select.disabled = true;

    // Turn on the menu checkbox and disable it for a menu or category type.
    if((type == 'menu') || (type == 'category'))
    { 
        document.edit_dialog.can_have_children.checked = true;
        document.edit_dialog.can_have_children.disabled = true;
    }

    if((type == "text") || (type == "menu"))
        // Disable the apply button if the text field is empty.
        document.edit_dialog.apply_edits.disabled = 
            isEmpty(document.edit_dialog.text);
    
    else if(type == "link")
    {
        // Disable the apply button if the link field is empty and the dialog 
        // is in link mode.
        document.edit_dialog.apply_edits.disabled = 
            isEmpty(document.edit_dialog.url) 
            || isEmpty(document.edit_dialog.text);

        // Allow click-through selection.
        document.edit_dialog.clickthrough_select.selectedIndex = 0;
        document.edit_dialog.clickthrough_select.disabled = false;
    }

    // Don't start off complaining.
    document.edit_dialog.text.style.background = 'white';
    document.edit_dialog.url.style.background = 'white';

    // Try to set focus to the appropriate field.
    if(document.edit_dialog.text.value.match('^\s*$'))
        document.edit_dialog.text.focus();
    else if(type == 'link')
        document.edit_dialog.url.focus();
}

// Is a field empty?
function isEmpty(field)
{
    var value = field.value;

    value = value.replace(/^ +/, '');
    value = value.replace(/ +$/, '');

    return !value;
}

// Validate a field.
function validateField(field)
{
    var empty = isEmpty(field);

    // Turn the field background pink if it is empty.
    field.style.background = empty ? 'pink' : 'white';

    return !empty;
}

// Validate the edit dialog.
function validateEditDialog()
{
    if(document.edit_dialog.textType.checked 
        || document.edit_dialog.menuType.checked
        || document.edit_dialog.categoryType.checked)
        // Disable the apply button if the text field is empty.
        document.edit_dialog.apply_edits.disabled = 
            !validateField(document.edit_dialog.text);

    else if(document.edit_dialog.linkType.checked)
        // Disable the apply button if the link field is empty and the dialog 
        // is in link mode.
        document.edit_dialog.apply_edits.disabled = 
            !validateField(document.edit_dialog.url) 
                || !validateField(document.edit_dialog.text);
}

// Show the editing dialog.
function showEditDialog(id)
{
    // Get information about the item and update the edit dialog.
    // Note that this is not asynchronous because I don't need that here.
    var xmlHttp = ajaxFunction('POST', serverURI, false, updateEditDialog);

    var params = new Array();

    params['action'] = 'query';
    params['id'] = id.substr(3);

    xmlHttp.invoke(params);

    // Go into update mode.
    document.edit_dialog.edit_action.value = 'update';

    // Set focus to the text field.
    document.edit_dialog.text.focus();

    showModalDialog('edit_dialog_background', 'edit_dialog');
}

// Ajax callback to update the edit dialog with the contents of the item.
function updateEditDialog(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // Get the item type and whether or not it can have children.
    var type = xmlDoc.getElementsByTagName('type')[0].childNodes[0].nodeValue;
    var can_have_children = 
        xmlDoc.getElementsByTagName('can_have_children')[0].childNodes[0].nodeValue;
    var child_count = 
        xmlDoc.getElementsByTagName('child_count')[0].childNodes[0].nodeValue;

    // Get the clickthrough id, which may be null.
    var clickthrough_id;

    var clickthrough_idElement =
        xmlDoc.getElementsByTagName('clickthrough_id')[0];

    if(clickthrough_idElement.childNodes.length)
        clickthrough_id = clickthrough_idElement.childNodes[0].nodeValue;

    // Update the type.
    document.edit_dialog.textType.checked = (type == 'text');
    document.edit_dialog.linkType.checked = (type == 'link');
    document.edit_dialog.menuType.checked = (type == 'menu');
    document.edit_dialog.categoryType.checked = (type == 'category');

    // Update the required values.
    document.edit_dialog.id.value = 
        'dl_' + xmlDoc.getElementsByTagName('id')[0].childNodes[0].nodeValue;
    document.edit_dialog.text.value = 
        xmlDoc.getElementsByTagName('text')[0].childNodes[0].nodeValue;
    document.edit_dialog.url.value = '';

    // Update the optional values.
    if(xmlDoc.getElementsByTagName('url')[0].childNodes.length)
        document.edit_dialog.url.value = 
            xmlDoc.getElementsByTagName('url')[0].childNodes[0].nodeValue;

    // Enable optional inputs.
    document.edit_dialog.url.disabled = (type != 'link');
    document.edit_dialog.can_have_children.checked = (can_have_children == 'Y');

    // By default, make sure no click-through is selected.
    document.edit_dialog.clickthrough_select.selectedIndex = 0;

    if(clickthrough_id)
    {
        // Find and select the clickthough that matches the one I'm 
        // looking for.
        var i;
        for(i = 0; i < document.edit_dialog.clickthrough_select.length; ++i)
          if(document.edit_dialog.clickthrough_select.options[i].value 
              == clickthrough_id)
              document.edit_dialog.clickthrough_select.selectedIndex = i;
    }

    // Enable inputs based on type.
    document.edit_dialog.textType.disabled = 
        ((type == 'menu') || (type == 'category'));
    document.edit_dialog.linkType.disabled = 
        ((type == 'menu') || (type == 'category'));
    document.edit_dialog.menuType.disabled = (type != 'menu');
    document.edit_dialog.categoryType.disabled = (type != 'category');
    document.edit_dialog.can_have_children.disabled = 
        ((type == 'menu') || (type == 'category') || (child_count > 0));
    document.edit_dialog.clickthrough_select.disabled = (type != 'link');
}

// Show the add dialog.
function showAddDialog(parent_id)
{
    // Get information about the item and update the edit dialog.
    // Note that this is not asynchronous because I don't need that here.
    var xmlHttp = ajaxFunction('POST', serverURI, false, updateAddDialog);

    var params = new Array();

    params['action'] = 'query';
    params['id'] = parent_id.substr(3);

    xmlHttp.invoke(params);

    // Go into add mode.
    document.edit_dialog.edit_action.value = 'add';

    // Set focus to the text field.
    document.edit_dialog.text.focus();

    showModalDialog('edit_dialog_background', 'edit_dialog');
}

// Ajax callback to display the add dialog.
function updateAddDialog(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // The item returned is the parent id. 
    var id = 'dl_' + xmlDoc.getElementsByTagName('id')[0].childNodes[0].nodeValue;
    var type = xmlDoc.getElementsByTagName('type')[0].childNodes[0].nodeValue;

    document.edit_dialog.parent_id.value = id;

    // Set some defaults.
    document.edit_dialog.categoryType.disabled = 0;
    document.edit_dialog.menuType.disabled = 0;
    document.edit_dialog.textType.disabled = 0;
    document.edit_dialog.linkType.disabled = 0;

    document.edit_dialog.url.disabled = 1;
    document.edit_dialog.apply_edits.disabled = 1;

    document.edit_dialog.text.value = '';
    document.edit_dialog.url.value = '';

    // The only possible child for root is category.
    if(type == 'root')
    {
        document.edit_dialog.categoryType.disabled = 1;
        document.edit_dialog.menuType.disabled = 1;
        document.edit_dialog.textType.disabled = 1;
        document.edit_dialog.linkType.disabled = 1;

        setEditDialogType('category');
    } 

    // A category can have any child except a category.
    else if(type == 'category')
    {
        document.edit_dialog.categoryType.disabled = 1;

        setEditDialogType('menu');
    }

    // A menu can have any child except a category or menu.
    else if(type == 'menu')
    {
        document.edit_dialog.categoryType.disabled = 1;
        document.edit_dialog.menuType.disabled = 1;
  
        setEditDialogType('link');
    }
    else
        setEditDialogType('link');
}

// Hide the editing dialog.
function hideEditDialog(xmlHttp)
{
    hideModalDialog('edit_dialog_background', 'edit_dialog');
}

// Apply edits.
function applyEdits()
{
    // Copy the user selections into an array.
    var params = new Array();

    params['action'] = document.edit_dialog.edit_action.value;

    params['id'] = document.edit_dialog.id.value.substr(3);
    params['parent_id'] = document.edit_dialog.parent_id.value.substr(3);

    if(document.edit_dialog.categoryType.checked)
        params['type'] = 'category';
    if(document.edit_dialog.menuType.checked)
        params['type'] = 'menu';
    if(document.edit_dialog.textType.checked)
        params['type'] = 'text';
    if(document.edit_dialog.linkType.checked)
        params['type'] = 'link';

    params['text'] = document.edit_dialog.text.value;
    params['url'] = document.edit_dialog.url.value;
    params['can_have_children'] = 
        document.edit_dialog.can_have_children.checked ? 'Y' : 'N';

    if(!document.edit_dialog.clickthrough_select.disabled)
        params['clickthrough_id'] =
            document.edit_dialog.clickthrough_select.options
                [document.edit_dialog.clickthrough_select.selectedIndex].value;

    document.edit_dialog.apply_edits.disabled = true;

    // Send the edits to the server.
    var httpXml = ajaxFunction('POST', serverURI, true, finishEdits);

    httpXml.invoke(params);
}

// Cancel edits.
function cancelEdits()
{
    hideEditDialog(); 
}

// Finish edits.
function finishEdits(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // Get the id.
    var id = 'dl_' + xmlDoc.getElementsByTagName('id')[0].childNodes[0].nodeValue;

    // Get the type.
    var type = xmlDoc.getElementsByTagName('type')[0].childNodes[0].nodeValue;

    // Get the html.
    var html = xmlDoc.getElementsByTagName('html')[0].childNodes[0].nodeValue;

    var element = document.getElementById(id);

    // If I am adding a line to a menu, adjust the returning HTML to set the correct
    // height and indicator to show the open menu with one additional line.
    if((type == "menu") && (document.edit_dialog.edit_action.value == 'add'))
        html = addMenuHtml(id, html);

    // Finger's crossed!
    element.innerHTML = html;

    hideEditDialog();
}

// Increase the size of a menu by one line while it is still in raw HTML.
function addMenuHtml(id, html)
{
    return adjustMenuHtml(id, html, lineHeight);
}

// Adjust the size of a menu while it is still in raw HTML.
function adjustMenuHtml(id, html, increment)
{
    // Get the id of the parent's menu content and indicator image.
    var menu_id = id + '_menu_content';
    var img_id = id + '_img';

    // The size of the current menu.
    var currentMenuSize = getMenuSize(document.getElementById(menu_id));

    // The new size of the menu.
    var newMenuSize = currentMenuSize + increment;

    // Fix the HTML before it gets displayed.
    html = html.replace('id="' + menu_id, 'id="' + menu_id + '" style="height:' + newMenuSize + 'px;');

    var oldImage = '<img id="' + img_id + '" alt="closed" src="images/tri.gif">';
    var newImage = '<img id="' + img_id + '" alt="open" src="images/tridown.gif">';

    return html.replace(oldImage, newImage);
}

// Show the delete dialog.
function showDeleteDialog(id)
{
    // Get information about the item and update the edit dialog.
    // Note that this is not asynchronous because I don't need that here.
    var xmlHttp = ajaxFunction('POST', serverURI, false, confirmDelete);

    var params = new Array();

    params['action'] = 'query';
    params['id'] = id.substr(3);

    xmlHttp.invoke(params);    
}

// Confirm a deletion.
function confirmDelete(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // Get the id.
    var id = 'dl_' + xmlDoc.getElementsByTagName('id')[0].childNodes[0].nodeValue;

    // Get the type.
    var type = xmlDoc.getElementsByTagName('type')[0].childNodes[0].nodeValue;

    // Get the text.
    var text = xmlDoc.getElementsByTagName('text')[0].childNodes[0].nodeValue;

    // Construct a confirmation message.
    var msg = 'Are you sure you want to delete the ' + type + ' "' + text + '" ';

    // Add additional info for a link.
    if(type == 'link')
    {
        var url = xmlDoc.getElementsByTagName('url')[0].childNodes[0].nodeValue;

        msg += 'linking to <' + url + '>';
    }

    msg += '?';

    // Get the number of affected rows.
    var child_count = 
        xmlDoc.getElementsByTagName('child_count')[0].childNodes[0].nodeValue;

    if(child_count > 0)
    {
        plural = (child_count > 1) ? 's' : '';

        msg += '\n\n' + child_count + ' child item' + plural + ' ';
        msg += 'will be deleted!';
    }

    if(confirm(msg))
    {
        var xmlHttp = ajaxFunction('POST', serverURI, true, reloadElement);

        var params = new Array();

        params['action'] = 'delete';
        params['id'] = id.substr(3);

        xmlHttp.invoke(params);    
    }
}

// Reload an element from the server.
function reloadElement(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // Get the id.
    var id = 'dl_' + xmlDoc.getElementsByTagName('id')[0].childNodes[0].nodeValue;

    // Get the type.
    var type = xmlDoc.getElementsByTagName('type')[0].childNodes[0].nodeValue;

    // Get the html.
    var html = xmlDoc.getElementsByTagName('html')[0].childNodes[0].nodeValue;

    var element = document.getElementById(id);

    // If this is a menu, decrease the size by one line and adjust the indicator
    // to show the open menu with one less line.
    if(type == "menu")
        html = deleteMenuHtml(id, html);

    // Good luck!
    element.innerHTML = html;
}

// Decrease the size of a menu by one line while it is still in raw HTML.
function deleteMenuHtml(id, html)
{
    return adjustMenuHtml(id, html, -lineHeight);
}

// Show a modal dialog, fading the dialog in and fading the background out.
function showModalDialog(backgroundID, dialogID)
{
    dimAnimation(document.getElementById(backgroundID), 0, 50);
    dimAnimation(document.getElementById(dialogID), 0, 100);
}

// Hide a modal dialog, fading out the dialog and fading in the background.
function hideModalDialog(backgroundID, dialogID)
{
    dimAnimation(document.getElementById(backgroundID), 50, 0);
    dimAnimation(document.getElementById(dialogID), 100, 0);
}

// Select a new clickthrough message.
function selectClickThrough(clickthrough_id)
{
    // The only thing I need to do here is prevent a clickthrough
    // display if the user selects "None" for a clickthrough.
    if(clickthrough_id)
        showClickThrough(clickthrough_id);
}

// Show a click through message to the user.
function showClickThrough(clickthrough_id)
{
    var xmlHttp = 
        ajaxFunction('POST', serverURI, false, displayClickThrough);

    var params = new Array();

    params['action'] = 'clickthrough';
    params['id'] = clickthrough_id;

    xmlHttp.invoke(params);
  
    return clickthroughResult;
}

// Display the click through message to the user.
function displayClickThrough(xmlHttp)
{
    // Get the XML contents of the result.
    var xmlDoc = xmlHttp.request.responseXML.documentElement;

    // Get the text.
    var text = 
        xmlDoc.getElementsByTagName('text')[0].childNodes[0].nodeValue;

    clickthroughResult = confirm(text);
}

