/*****************************
    Various shared JavaScript items

    Created by: Kevin Monahan, kevin(at)snoringcatphotography(dot)com
    Licensing: Licensed under the terms of the LGPL. If you use this file
               or any portions of it please retain this header section,
               including the licensing.
******************************/

/**
 * A simple class that mimics a java StringBuffer
 */
function StringBuilder( initialValue )
{
    this.parts = [];
    if ((typeof initialValue != "undefined") && !! initialValue)
    {
        this.parts.push( initialValue );
    }
}

/**
 * Append a new item to the string being assembled.
 * @param part
 */
StringBuilder.prototype.append = function( part )
{
    this.parts.push( part );

    // So we can chain append operations.
    return this;
}

/**
 * Combine our array of parts into a complete string.
 */
StringBuilder.prototype.toString = function()
{
    return this.parts.join( '' );
}

/**
 * A class that wraps various logging methods. The best is to
 * use Firebug's console.
 */

// The log levels we support, a la log4j.
Logger.none = 0;
Logger.error = 1;
Logger.warn = 2;
Logger.info = 3;
Logger.debug = 4;

// WHen we write our log messages these are the strings we use to report the level of the message.
Logger.levelStrings = [ "", "ERROR", "WARN", "INFO", "DEBUG" ];

/**
 * Instantiates the logger for the page.
 * @param level One of the logging levels. Log messages will only
 *              be logged when they are equal to or higher than this
 *              level.
 */
function Logger( level )
{
    // The current log level
    this.level = level;

    this.logger = null;

    if ((typeof( console ) != "undefined") || (typeof(window.console) != "undefined"))
    {
        this.logger = Logger.logToFirebug;
    }
}

/* public */
/**
 * Logs a message if an appropriate logging location can
 * be found.
 * @param level The current log level, a la log4j
 * @param message Either a string that contains the message to log
 *                  or a function that will be called if logging is
 *                  enabled. The return value of this function will
 *                  be written to the log.
 */
Logger.prototype.log = function( level, message )
{
    // Can we log anything?
    if (!! this.logger)
    {
        // Can we log at this level?
        if (this.level <= level)
        {
            // Write the log message.
            this.logger( level, !! message ? (typeof message == "function" ? message() : message) : "" );
        }
    }
}

/* public */
// Methods for logging at specific levels.
Logger.prototype.error = function( message ) { this.log( Logger.info, message ); }
Logger.prototype.warn = function( message ) { this.log( Logger.warn, message ); }
Logger.prototype.info = function( message ) { this.log( Logger.info, message ); }
Logger.prototype.debug = function( message ) { this.log( Logger.debug, message ); }

/* private */
/**
 * Write the log message to the Firebug console.
 * @param level The level; here used only to determine the level name to display.
 * @param message The message string. By this time it is no longer a function, if
 *                  it was originally passed that way.
 */
Logger.logToFirebug = function ( level, message )
{
    var stringToLog = Logger.getDateTimeString() + " " + Logger.levelStrings[level] + ": " + message;
    switch (level)
    {
        case Logger.error:
            console.error( stringToLog );
            break;
        case Logger.warn:
            console.warn( stringToLog );
            break;
        case Logger.info:
            console.info( stringToLog );
            break;
        case Logger.debug:
            console.debug( stringToLog );
            break;
    }
    return stringToLog;
}

// Constants used in creating the date/time string
Logger.months = [ "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12" ];
Logger.days = [ "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31" ];
Logger.hours = [ "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23" ];
Logger.minutesSeconds = [ "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59" ];

/* private */
/**
 * Returns the current date and time formatted as a string.
 */
Logger.getDateTimeString = function()
{
    var now = new Date();
    var ms = now.getMilliseconds();

    return new StringBuilder()
        .append( now.getYear() + 1900 ).append( '/' )
        .append( Logger.months[now.getMonth()] ).append( '/' )
        .append( Logger.days[now.getDay()] ).append( ' ' )
        .append( Logger.hours[now.getHours()] ).append( ':' )
        .append( Logger.minutesSeconds[now.getMinutes()] ).append( ':' )
        .append( Logger.minutesSeconds[now.getSeconds()] ).append( '.' )
        .append( ms == 0 ? "000" : ms < 10 ? "00" : ms < 100 ? "0" : "" ).append( ms )
        .toString();
}

// Various useful utilities.

/**
 * Searches up the DOM hierarchy starting with the target and looking
 * for a parent that "is" the selector. The target is checked first.
 * @param target
 * @param selector
 * @returns the jQuery set for the first DOM element that matches or null if a match isn't found.
 */
function findSelector( target, selector )
{
    if (! target || (target.is( selector )))
    {
       return target;
    }

    // Target didn't match. Start searching up the DOM hierarchy for a match. If we reach the
    // end then return the document.
    var parent = target.parent();
    while (!! parent && ! parent.is( selector ) && (typeof parent[0] != "undefined") && (parent[0].tagName != "BODY"))
    {
        parent = parent.parent();
    }

    return ((typeof parent[0] == "undefined") || (parent[0].tagName == "BODY")) ? null : parent;
}

/**
 * Return true if variable is both defined and not null.
 * @param variable
 */
function notNull( variable )
{
    return (typeof variable != "undefined") && !! variable;
}

/**
 * Similar to notNull but also verifies if the variable has contents.
 * This function assumes that you're passing in a string.
 */
function hasContents( variable )
{
    return (typeof variable == "string") && !! variable && (variable.length > 0);
}
/**
 * Simply returns false. Handy if you want a callback that will just return false;
 */
function returnFalse() { return false; }

/**
 * Constructs an email address dynamically so that a static
 * address does not have to appear in the code.
 * @param address The email address,in the form <local name>(at)<domain name>(dot)com
 */
function generateEmailAddress( address, friendlyName )
{
    var returnVal = address.replace( /\(at\)/, "@" ).replace( /\(dot\)/g, "." );
    if (hasContents( friendlyName ))
    {
        returnVal = '(' + friendlyName + ") " + returnVal;
    }
    return returnVal;
}

/**
 * Opens a compose window using the local email client. The supplied email
 * address is put into the TO field.
 * @param address The email address,in the form <local name>(at)<domain name>(dot)com
 * @param subject An optional subject that will be included in the mailto link
 */
function displayEmailComposeWindow( address, subject, friendlyName )
{
    var mailToURL = "mailto:" + generateEmailAddress( address, friendlyName );
    if (hasContents( subject ))
    {
        mailToURL += "?subject=" + subject
    }
    window.location = mailToURL;
}

/**
 * A class that implements a simple slide show of images. For now, a simple tool
 * that does not attempt to cache images, etc.
 * @param main The img tag where we display the slides
 * @param buffer A hidden img tag where we preload the slides.
 * @param slides An array of slides, identified by URLs, that we load.
 */
function SlideShow( main, buffer, slides )
{
    // When true the slideshow will execute
    this.run = false;

    this.mainImgElement = $( main );
    this.bufferImgElement = $( buffer );

    this.slides = slides;
    this.currentSlideIndex = 0;
    this.delay = 5000;
    this.showNextSlideAt = 0;
}

/**
 * Starts the slide show.
 */
SlideShow.prototype.start = function()
{
    this.run = true;
    this.prepareForNextSlide();
}

/**
 * Stops the slideshow.
 */
SlideShow.prototype.stop = function()
{
    this.run = false;
}

/* private */
/**
 * Preloads the next slide. WHen the interval until we should
 * show the slide expires we call showSlide to actually flip
 * the new image into view.
 */
SlideShow.prototype.prepareForNextSlide = function()
{
    this.showNextSlideAt = new Date().getTime() + this.delay;
    var img = document.createElement( "img" );
    var $this = this;
    img.onload =
        function()
        {
            // The image has been loaded. When the interval has elapsed swap it in.
            var now = new Date().getTime();
            setTimeout(
                function()
                {
                    if ($this.run)
                    {
                        $this.showSlide( img );
                    }
                },
                // Always enter the timer code so that we don't get recursion.
                // Not sure if this will really work should the setTimeout call
                // determine that the interval is already over before it has queued
                // the request.
                now > $this.showNextSlideAt ? 1 : $this.showNextSlideAt - now
            );
        }
    img.src = this.slides[this.currentSlideIndex++];
    if (this.currentSlideIndex == this.slides.length)
    {
        this.currentSlideIndex = 0;
    }
}

/* private */
/**
 * Flips the new image into view and then starts the timer for
 * the next interval.
 * @param img
 */
SlideShow.prototype.showSlide = function( img )
{
    this.bufferImgElement[0].src = img.src;
    this.mainImgElement.fadeOut( 500 );
    this.bufferImgElement.fadeIn( 500 );

    // Swap our buffers.
    var temp = this.mainImgElement;
    this.mainImgElement = this.bufferImgElement;
    this.bufferImgElement = temp;

    // Start the wait for the next slide.
    this.prepareForNextSlide();
}


/**
 * A simple class that defines a tab bar. It will load and hide
 * tab content as the tabs are clicked.
 *
 * @param tabWrapper Identifies the DIV that contains the tab "handles"
 * @param contentWrapper Identifies the DIV where the content for each
 *                      tab is displayed
 * @param initialTab The identifier of the tab we first select.
 * @param options Options for the tabs and class
 */
function TabBar( tabWrapper, contentWrapper, options )
{
    var localThis = this;

    // The div where the tab "handles" are displayed. We also keep a reference to
    // our object here.
    this.tabWrapper = $( tabWrapper );
    this.tabWrapper[0].TabBar = this;

    // The div where we put the tab contents
    this.contentWrapper = $( contentWrapper );

    // The name of the tab that is currently being displayed.
    this.currentTab = null;
    // Our options
    this.options = options;

    // Assign handlers to each of the tabs.
    var tabs = this.tabWrapper.children( ".tab" );
    tabs.bind( "click", this, TabBar.selectTab )
        .bind( "mouseover", this, TabBar.handleMouseOver )
        .bind( "mouseout", this, TabBar.handleMouseOut );

    // Display the initial tab.
    this.displayTab( this.tabWrapper.find( ".currentTab" ).attr( "id" ) );
}

/**
 * Given a reference to something inside the tab bar return the actual TAbBar object.
 * @param target Some element (a jQuery set) within the tab.
 */
TabBar.getObject = function( target )
{
    return findSelector( target, ".tabBar" )[0].TabBar;
}

TabBar.handleMouseOver = function( event )
{
    // Whenever we get a mouseover event we know that we're either entering the
    // tab or reentering it from a child element. What we really need to know is
    // whether our subtab is already showing. If it is then we do nothing. If it
    // isn't then we show it.

    // Do the "withParent" in case we're in a child of the tab; for example, the a tag.
    var currentTab = findSelector( $( event.target ), ".tab" );
    var subTabBar = currentTab.find( ".subTabBar" );
    // No sub tabbar? Sub tab bar but it is currently being shown? Just exit.
    if ((subTabBar.length != 0) && ! subTabBar.hasClass( "subTabShowing" ))
    {
        // Hide any other subtab bar that might be visible and show ours.
        event.data.hideSubTabBars().showSubTabBar( currentTab, subTabBar );
    }

    return false;
}

TabBar.prototype.showSubTabBar = function( currentTab, subTabBar )
{
    subTabBar.addClass( "subTabShowing" ).show( 0 );
    return this;
}

/**
 * Hide either the supplied tab's subtab bar or all of the subtabs in the tab bar.
 * @param tab The tab whose subtab we want to remove. Can be null or undefined
 *              if all subtabs should be removed.
 */
TabBar.prototype.hideSubTabBars = function( tab )
{
    // Either remove this tab's subtab bar or all of the subtab bars.
    if (notNull( tab ))
    {
        findSelector( tab, ".tab" ).find( ".subTabBar" ).hide( 0 ).removeClass( "subTabShowing" );
    }
    else
    {
        this.tabWrapper.find( ".tab" ).find( ".subTabBar" ).hide( 0 ).removeClass( "subTabShowing" );
    }

    return this;
}

TabBar.handleMouseOut = function( event )
{
    // We are only interested in mouse outs where the element we are moving into
    // is not in our tab's DOM hierarchy.
    var tab = findSelector( $( event.relatedTarget ), ".tab" );

    // Not in any tab or subtab?
    if (! tab)
    {
        event.data.hideSubTabBars();
    }
    else
    {
        // We are still in a tab. We need to see if it is "our" tab or have we
        // moved out of the tab onto another tab?
        // not handling leaving the subtab correctly***
        var currentTab = findSelector( $( event.target ), ".tab" );
        if (tab.attr( "id" ) != currentTab.attr( "id" ))
        {
            event.data.hideSubTabBars( currentTab );
        }
    }

    return false;
}

/**
 * Fetch a tab's contents from the server and display it in the content area.
 * @param tabName
 */
TabBar.prototype.displayTab = function( tabName )
{
    logger.info( "Retrieving the data for the " + tabName + " tab." );

    // If there is a current tab then call it's unload handler.
    var tabData;
    if (!! this.currentTab)
    {
        tabData = this.options.tabs[this.currentTab];
        if (!! tabData.handlers && !! tabData.handlers.unload)
        {
            tabData.handlers.unload( this, tabData );
        }
    }

    // Fetch the data for the new tab. If the user clicked on the
    // main tab bar we might actually default them to one of the
    // subtabs
    tabData = this.options.tabs[tabName];
    // Some times the main tab click defaults to one of the subtabs.
    if (tabData.defaultSubTab != undefined)
    {
        tabName = tabData.defaultSubTab;
        tabData = this.options.tabs[tabName];
    }
    sendAjaxRequest(
        "Get tab contents",
        tabData.url,
        null,
        TabBar.dataReceived,
        TabBar.dataUnavailable,
        {
            tabBar: this,
            tabName: tabName
        }
    )
}

var clickTab = function( tabId )
{
    var tabObject = $( "#mainTabBar" )[0].TabBar;
    tabObject.displayTab( tabId );
    tabObject.hideSubTabBars();
}

TabBar.selectTab = function( event )
{
    var target = $( event.target );
    var tabId;
    var selectedTab = findSelector( target, ".subTab" );
    if (selectedTab != null)
    {
        tabId = selectedTab.attr( "id" );
    }
    else
    {
        tabId = findSelector( target, ".tab" ).attr( "id" );
    }
    event.data.displayTab( tabId );
    event.data.hideSubTabBars();
}

TabBar.dataReceived = function( xhr, data )
{
    var tabBar = data.tabBar;
    tabBar.contentWrapper.html( xhr.responseText );

    // Call the tab's load function.
    var tabData = tabBar.options.tabs[data.tabName];
    if (!! tabData.handlers && !! tabData.handlers.load)
    {
        tabData.handlers.load( tabBar, tabData );
    }
    tabBar.currentTab = data.tabName;

    // Update the styles on the tabs.
    var currentTab = tabBar.tabWrapper.find( ".currentTab" );
    if (currentTab.attr( "id" ) != data.tabName)
    {
        currentTab.removeClass( "currentTab" );
    }
    tabBar.tabWrapper.find( '#' + data.tabName ).addClass( "currentTab" );
}

TabBar.dataUnavailable = function( xhr, data )
{
    alert( "Cannot get the data for the tab: " + xhr.status );
}

/**
 * Sends a simple Ajax request with basic data.
 * @param url
 * @param data
 * @param success
 * @param failure
 */
function sendAjaxRequest( name, url, parameters, successFcn, errorFcn, data )
{
    if (!! name)
    {
        logger.info( "Starting Ajax request: name = " + name + ", URL = " + url );
    }
    var xhr = $.ajax(
        {
            url: url,
            type: "get",
            dataType: "html",
            data: parameters,
            success: function()
                {
                    if (!! name)
                    {
                        logger.info( "Ajax request complete: name = " + name + ", URL = " + url );
                    }
                    if (!! successFcn)
                    {
                        successFcn( xhr, data );
                    }
                },
            error: function()
                {
                    if (!! name)
                    {
                        logger.info( "Ajax request failed: status = " + xhr.status + ", name = " + name + ", URL = " + url );
                    }
                    if (!! errorFcn)
                    {
                        errorFcn( xhr, data );
                    }
                }
        }
    );

    return xhr;
}
