﻿/// <reference name="MicrosoftAjax.js"/>
/// <reference name="~/assets/javascript/json.js"/>

/*jslint browser: true*/

/*global

    $addHandlers            $clearHandlers      $get
    NeighborhoodPreviews    Sys                 Type
    Website                 $addHandler         NP_ImageEditorPopup
    JSON                    window

*/

Type.registerNamespace("Website");

// A utility method to encode HTML data.
function np_encodeToHtml( string )
{
    var i;
    var output = "";
    
    for( i = 0; i < string.length; i++ )
    {
        var c = string.charAt( i );
        
        if( c == "<" ) { output += "&lt;"; }
        else if( c == ">" ) { output += "&gt;"; }
        else if( c == "&" ) { output += "&amp;"; }
        else { output += c; }
    }
    
    return output;
}

// A utility method to create an OPTION element.
function np_createOption( value, textEncoded )
{
    var option = document.createElement( "option" );
    option.value = value;
    option.text = textEncoded;
    
    return option;
}

// A utility method to add an option to the end of a select element.
function np_addOption( select, value, text )
{
    var option = np_createOption( 
        value, 
        np_encodeToHtml( text ) 
        );
                
    try 
    {
        // Standards compliant.
        select.add( option, null );
    }
    catch( ex )
    {
        // IE only
        select.add( option, select.length );
    }
}

// This class provides a single image editing dialog that can be used by all 
// image content on a page. Note that this class creates some UI elements, all
// of which are configured using CSS classes. Those classes and the structure
// of the HTML that is created is defined below:
//
//     div class="imageEditorPopup_container"
//     |            This is the container DIV, and should fill the entire 
//     |            screen when visible. It will be created with its DISPLAY 
//     |            rule initially set to NONE.
//     |
//     |- div class="imageEditorPopup_backgroundDiv"
//     |            This is a DIV provided in case the designer wants to render
//     |            a floating background or grayed out image.
//     |
//     |- div class="imageEditorPopup_popupDiv"
//        |         This DIV is the main container for all of the UI elements. 
//        |         Ideally, it should float in the center of the screen.
//        |
//        |- div class="imageEditorPopup_imageEditDiv"
//        |  |
//        |  |- img class="imageEditorPopup_selectedImg" 
//        |  |  id="imageEditorPopup_selectedImg"
//        |  |      This IMG tag will display the currently selected image.
//        |  |      The server will render this image to fill as much of the
//        |  |      offsetWidth and offsetHeight (BE CAREFUL OF PADDING AND
//        |  |      MARGINS!) of its parent DIV. Initially, and whenever the
//        |  |      user changes the currently selected image, this image will
//        |  |      be replaced with a "wait" icon. During such times, the 
//        |  |      crop rectange will not be visible.
//        |  |
//        |  |- div class="imageEditorPopup_cropRectangle" 
//        |     id="imageEditorPopup_cropRectangle"
//        |         This DIV represents the current crop applied to the IMG.
//        |         Its width and height will be adjusted via javascript as
//        |         the end-user adjusts the crop.
//        |
//        |- div class="imageEditorPopup_altTextDiv"
//        |  |
//        |  |- label class="imageEditorPopup_altTextLabel"
//        |  |      This is the LABEL for the alternate text input field, and
//        |  |      will contain the text "Alternate Text" (without the 
//        |  |      quotes).
//        |  |
//        |  |- input class="imageEditorPopup_altTextTextBox"
//        |     id="imageEditorPopup_altTextTextBox"
//        |         This is the input textbox that will accept the value for 
//        |         the images alternate text (and caption).
//        |
//        |- div class="imageEditorPopup_filterDiv"
//        |  |
//        |  |- span class="imageEditorPopup_filterHeading"
//        |  |      This SPAN contains the text "Filter" (without the quotes).
//        |  |
//        |  |- label class="imageEditorPopup_nameLabel"
//        |  |      This is the LABEL for the image name search box, and will
//        |  |      contain the text "Image Name" (without the quotes).
//        |  |
//        |  |- input class="imageEditorPopup_nameTextBox" 
//        |  |  id="imageEditorPopup_nameTextBox"
//        |  |      This is the input textbox that will accept the user's 
//        |  |      filter for image names. As the user types in this box,
//        |  |      the list of applicable images shown to her will be filtered
//        |  |      on the fly.
//        |  |      
//        |  |      If the control is in the process of applying a filter that
//        |  |      the user has entered, the class for this textbox will be
//        |  |      switched to "imageEditorPopup_nameTextBox_filtering".
//        |  |
//        |  |- span class="imageEditorPopup_tagsLabel"
//        |  |      This SPAN will have the text "Tags" (without the quotes).
//        |  |
//        |  |- div class="imageEditorPopup_filterTagContainer"
//        |     |
//        |     |- span class="imageEditorPopup_filterTag"
//        |         This SPAN will contain an input of type check-box, and a
//        |         LABEL that defines the name of the tag associated with the
//        |         checkbox. There will be one such SPAN for each tag defined
//        |         in the system.
//        |
//        |- div class="imageEditorPopup_emptyImageList" 
//        |  id="imageEditorPopup_emptyImageList"
//        |         This DIV contains the text "Sorry. There are no images
//        |         that match your current filter criteria." (without the 
//        |         quotes). As this text implies, it will be made visible
//        |         whenever the user's current filter criteria does not apply
//        |         to any uploaded image. In any other scenario, the sister
//        |         DIV, imageEditorPopup_imageList, will be visible.
//        |
//        |- div class="imageEditorPopup_imageList" 
//           id="imageEditorPopup_imageList"
//           |     
//           |- div class="imageEditorPopup_sortControls"
//           |  |
//           |  |- input
//           |  |   This INPUT has the value "Sort By Date Uploaded" (without
//           |  |   the quotes).
//           |  |
//           |  |- input
//           |  |   This INPUT has the value "Sort By Name" (without the 
//           |  |   quotes).
//           |  |
//           |  |- input
//           |  |   This INPUT has the value "Save Changes" (without the
//           |  |   quotes).
//           |  |
//           |  |- input
//           |      This INPUT has the value "Cancel Changes" (without the
//           |      quotes).
//           |
//           |- div class="imageEditorPopup_thumbnailContainer" 
//              id="imageEditorPopup_thumbnailContainer"
//              |
//              |- span class="imageEditorPopup_thumbnail" (or) 
//                 class="imageEditorPopup_selectedThumbnail"
//                  This SPAN contains the an inner SPAN which includes the 
//                  name of the image followed by an IMG that shows its 
//                  thumbnail. Its class will be set to ...selectedThumbnail
//                  whenever the image it contains is the currently selected
//                  thumbnail. There will be one such span for each image that
//                  matches the currently entered search criteria.
//
var ___NP_thePopup = null;
NP_ImageEditorPopup = function( spinnerAssetPath )
{
    this._spinnerAssetPath = spinnerAssetPath;

    // Create the main DIV, set its innerHTML, and apply it to the very
    // end of the document.
    this._mainDiv = document.createElement( "DIV" );
    this._mainDiv.className = "imageEditorPopup_container";
    this._mainDiv.style.display = "none";
    this._mainDiv.innerHTML =
        "<div class='imageEditorPopup_backgroundDiv'>&nbsp;</div>" +
        "<div class='imageEditorPopup_popupDiv'>" +
            "<div class='imageEditorPopup_imageEditDiv' id='imageEditorPopup_imageEditDiv' style='position: relative;'>" +
                "<img class='imageEditorPopup_selectedImg' id='imageEditorPopup_selectedImg' src='" + spinnerAssetPath + "' />" +
                "<div class='imageEditorPopup_cropRectangle' id='imageEditorPopup_cropRectangle' style='display: none; position: absolute; width: 100px; height: 100px; top: 0px; left: 0px;'>&nbsp;</div>" +
            "</div>" +
            "<div class='imageEditorPopup_altTextDiv'>" +
                "<label for='imageEditorPopup_altTextTextBox' class='imageEditorPopup_altTextLabel'>Alternate Text</label>" +
                "<input type='text' class='imageEditorPopup_altTextTextBox' id='imageEditorPopup_altTextTextBox' />" +
            "</div>" +
            "<div class='imageEditorPopup_filterDiv'>" +
                "<span class='imageEditorPopup_filterHeading'>Filter</span>" +
                "<label class='imageEditorPopup_nameLabel' for='imageEditorPopup_nameTextBox'>Image Name</label>" +
                "<input type='text' class='imageEditorPopup_nameTextBox' id='imageEditorPopup_nameTextBox' title='Characters other than letters and numbers will be ignored.' />" +
                "<span class='imageEditorPopup_tagsLabel'>Tags</span>" +
                "<div class='imageEditorPopup_filterTagContainer' id='imageEditorPopup_filterTagContainer'>" +
                    "<img src='" + spinnerAssetPath + "' />" +
                "</div>" +
            "</div>" +
            "<div class='imageEditorPopup_emptyImageList' id='imageEditorPopup_emptyImageList' style='display: none'>" +
                "Sorry. There are no images that match your current filter criteria." +
            "</div>" +
            "<div class='imageEditorPopup_imageList' id='imageEditorPopup_imageList'>" +
                "<div class='imageEditorPopup_sortControls'>" +
                    "<input type='button' value='Sort by Date Uploaded' id='imageEditorPopup_sortByDateButton' />" +
                    "<input type='button' value='Sort by Name' id='imageEditorPopup_sortByNameButton' />" +
                    "<input type='button' value='Save Changes' id='imageEditorPopup_saveChangesButton' />" +
                    "<input type='button' value='Cancel Changes' id='imageEditorPopup_cancelChangesButton' />" +
                "</div>" +
                "<div class='imageEditorPopup_thumbnailContainer' id='imageEditorPopup_thumbnailContainer'>" +
                    "<img src='" + spinnerAssetPath + "' />" +
                "</div>" +
            "</div>" +
        "</div>";
        
    document.body.appendChild( this._mainDiv );
    this._selectedImg = $get( "imageEditorPopup_selectedImg" );
    this._cropRectangleDiv = $get( "imageEditorPopup_cropRectangle" );
    this._filterTagDiv = $get( "imageEditorPopup_filterTagContainer" );
    this._emptyImageDiv = $get( "imageEditorPopup_emptyImageList" );
    this._imageDiv = $get( "imageEditorPopup_imageEditDiv" );
    this._imageListDiv = $get( "imageEditorPopup_imageList" );
    this._thumbnailContainer = $get( "imageEditorPopup_thumbnailContainer" );
    this._altTextbox = $get( "imageEditorPopup_altTextTextBox" );
    this._filterTextbox = $get( "imageEditorPopup_nameTextBox" );
    this._filterTagContainer = $get( "imageEditorPopup_filterTagContainer" );
    
    // Attach some event handlers to our sort buttons.
    $get( "imageEditorPopup_sortByDateButton" ).onclick = function() {
            if( ___NP_thePopup._sortMode == "date" ) {
                ___NP_thePopup._sortAscending = !___NP_thePopup._sortAscending;
            }
            else {
                ___NP_thePopup._sortMode = "date";
                ___NP_thePopup._sortAscending = true;
            }
            
            ___NP_thePopup.startWaiting( false );
        };
    $get( "imageEditorPopup_sortByNameButton" ).onclick = function() {
            if( ___NP_thePopup._sortMode == "name" ) {
                ___NP_thePopup._sortAscending = !___NP_thePopup._sortAscending;
            }
            else {
                ___NP_thePopup._sortMode = "name";
                ___NP_thePopup._sortAscending = true;
            }
            
            ___NP_thePopup.startWaiting( false );
        };
    $get( "imageEditorPopup_cancelChangesButton" ).onclick = function() { ___NP_thePopup.close(); };
    $get( "imageEditorPopup_saveChangesButton" ).onclick = function() { ___NP_thePopup.saveChanges(); };
    
    // Attach the scroll event handler to imageEditorPopup.
    this._thumbnailContainer.onscroll = function() {
        var diff = ___NP_thePopup._thumbnailContainer.scrollHeight - ___NP_thePopup._thumbnailContainer.scrollTop;
        if( diff <= ___NP_thePopup._thumbnailSize * 3 ) {
            ___NP_thePopup.loadNextBatch();
        }
    };
    
    // Attach some event handlers to the text box.
    this._filterTextbox.onchange = this.handleTextChange;
    this._filterTextbox.onkeyup = this.handleTextChange;
    
    // Attach the resize handlers to our crop div.
    $addHandlers(
        document, 
        {
            'mousemove':    this.cropMouseMove,
            'mouseup':      this.cropMouseUp,
            'mousedown':    this.cropMouseDown
        },
        this );
};

NP_ImageEditorPopup.prototype = 
{
    _mainDiv : null,
    
    _selectedImg : null,
    
    _cropRectangleDiv : null,
    
    _filterTagDiv : null,
    
    _imageDiv : null,
    
    _thumbnailContainer : null,
    
    _altTextbox : null,
    
    _filterTextbox : null,
    
    /**
     * The DIV that contains the list of thumbnail images and sort
     * controls.
     */
    _imageListDiv : null,
    
    /**
     * The master list of images retrieved from the server.
     */
    _serverImageList : [],
    
    /**
     * The list of images currently displayed to the user.
     */
    _currentImageList : [],
    
    _sourceImage : null,
    
    _spinnerAssetPath : null,
    
    _lastLoadedIndex : -1,
    
    _selectedImage : null,
    
    _filterTagContainer : null,
    
    /**
     * The dimensions of the image thumbnails (width == height)
     */
    _thumbnailSize : 50,
    
    /**
     * The number of image elements to add to the GUI at once.
     */
    _metaBatchSize : 40,
    
    /**
     * Controls the number of images that the popup will try to load in its 
     * thumbnail list simultaneously.
     */
    _thumbnailLoadBatchSize : 5,
    
    /**
     * The current number of thumbnail images that are currently downloading
     * from the server. When this value equals zero, any download batches have
     * completed.
     */
    _currentThumbnailLoadCount : 0,
    
    /**
     * The last time the user made a change to one of the filter criteria. This
     * is used to wait for an appropriate amount of time before applying a 
     * filter.
     */
    _lastFilterChange : new Date(),
    
    /**
     * This will be true IFF the control is in the process of filtering results
     * for the user.
     */
    _filtering : false,
    
    /**
     * An array of the tag input boxes added to the filter section of the 
     * control.
     */
    _tagInputs : [],
    
    /**
     * The string we're currently filtering against. Will be null if there's 
     * no need to filter against strings.
     */
    _currentFilterString : "",
    
    /**
     * The current list of tags we're filtering against. Will have a bunch of
     * properties as in tag0 : false, tag3 : true, etc. Each property 
     * corresponds to the database ID of a particular tag.
     */
    _currentTagSet : null,
    
    /**
     * The index of the current filter batch. The start index from the master 
     * image list is computed as _currentFilterBatch * 100.
     */
    _currentFilterBatch : 0,
    
    /**
     * Will be true if the waitToFilter function is in its loop and false 
     * otherwise.
     */
    _inWaitLoop : false,
    
    /**
     * Defines the currently selected sort mode. "none" indicates that we're
     * displaying them in the order we got them. "date" indicates that we're
     * sorting by date uploaded, and "name" indicates we're sorting by name.
     */
    _sortMode : "none",
    
    /**
     * Indicates the direction of sort. Ascending is the default order.
     */
    _sortAscending : true, 
    
    /**
     * Flag that indicates if the user is dragging the crop rectangle around.
     */
    _cropDragging : false,
    
    /**
     * The current type of dragging that's going on. "move" means moving the
     * whole rectangle, "right" means dragging the bottom/right side.
     */
    _cropDragMode : null,
    
    _cropDragStartClientX : 0,
    _cropDragStartClientY : 0,
    _cropDragStartOffsetX : 0,
    _cropDragStartOffsetY : 0,
    _cropDragStartWidth : 0,
    _cropDragStartHeight : 0
};

/**
 * Removes all non-word characters from the provided string.
 */
NP_ImageEditorPopup.prototype.simplify = function( string ) {
    var output = "";

    for( var i = 0; i < string.length; i++ ) {
        var c = string[ i ];
        
        if( c.search( /\w/ ) != -1 ) {
            output += c.toLowerCase();
        }
    }
    
    return output;
};

/**
 * Returns true IFF the image editor is done loading thumbnails for the current
 * batch.
 */
NP_ImageEditorPopup.prototype.isDoneLoading = function() {
    return this._currentThumbnailLoadCount <= 0;
};

/**
 * Does the actual filtering work, after being initialized by the .filter
 * function. Processes the source image batch 100 images at a time and waits
 * 100ms between batches.
 */
NP_ImageEditorPopup.prototype.filterWorker = function() {

    // Process images one at a time.
    for( var i = 0; i < 100; i++ ) {
        var j = (___NP_thePopup._currentFilterBatch * 100) + i;
        
        // Are we done?
        if( j >= ___NP_thePopup._serverImageList.length ) { break; }
        
        // Grab this image from the list and check it against the filter.
        var image = ___NP_thePopup._serverImageList[ j ];
        image.visible = false;
        var include = true;
        
        // Filter against the name.
        if( ___NP_thePopup._currentFilterString !== null ) {
            if( image.simplifiedName === undefined || image.simplifiedName === null ) {
                image.simplifiedName = ___NP_thePopup.simplify( image.imageName );
            }
            
            if( image.simplifiedName.indexOf( this._currentFilterString ) == -1 ) {
                include = false;
            }
        }
        
        // Now make sure that at least one of the tags is selected.
        if( include ) {
            var atLeastOne = false;
            for( var k = 0; k < image.tags.length; k++ ) {
                var tagName = "tag" + image.tags[ k ];
                if( ___NP_thePopup._currentTagSet[ tagName ] === true ) {
                    atLeastOne = true;
                    break;
                }
            }
            
            if( !atLeastOne ) { include = false; }
        }
        
        // Add it to the current image list?
        if( include ) {
            ___NP_thePopup._currentImageList.push( image );
        }
    }
    
    // Queue the next worker.
    ___NP_thePopup._currentFilterBatch++;
    if( ___NP_thePopup._currentFilterBatch < ___NP_thePopup._serverImageList.length ) { 
        window.setTimeout( ___NP_thePopup.filterWorker, 100 );
    } 
    else {
        // All done.
        ___NP_thePopup._filtering = false;
        ___NP_thePopup._thumbnailContainer.innerHTML = "";
        ___NP_thePopup._filterTextbox.className = "imageEditorPopup_nameTextBox";
        
        if( ___NP_thePopup._currentImageList.length === 0 ) {
            // Too filtered.
            ___NP_thePopup._emptyImageDiv.style.display = "";
            ___NP_thePopup._imageListDiv.style.display = "none";
        }
        else {
            // Sort the images.
            if( ___NP_thePopup._sortMode != "none" ) {
                ___NP_thePopup._currentImageList.sort( function(a, b) {
                        var compare = 0;
                        
                        if( ___NP_thePopup._sortMode == "date" ) {
                            // Sort by date.
                            compare = b.uploadDate.getTime() - a.uploadDate.getTime();
                        }
                        else {
                            // Sort by name.
                            compare = a.imageName.localeCompare( b.imageName );
                        }
                        
                        if( !___NP_thePopup._sortAscending ) {
                            compare = -1 * compare;
                        }
                        
                        return compare;
                    } );
            }
        
            // At least one image to show.
            ___NP_thePopup._emptyImageDiv.style.display = "none";
            ___NP_thePopup._imageListDiv.style.display = "";
            ___NP_thePopup.loadNextBatch();
        }
    }
};

/**
 * Filters the list of displayed images according to the user's currently
 * selected criteria.
 */
NP_ImageEditorPopup.prototype.filter = function() {
    var i;
    
    // First, get the name we're filtering against.
    var filterString = null;
    
    if( this._filterTextbox.value !== "" ) {
        filterString = this.simplify( this._filterTextbox.value );
    }
    
    // Next, build a map of the tags that we're including. Will be as in
    // "tag1", "tag2", etc., where the indices are the tag ID from the
    // database.
    var tagSet = {};
    
    for( i = 0; i < this._tagInputs.length; i++ ) {
        var tagInput = this._tagInputs[ i ];
        var sourceTag = tagInput.sourceTag;
        
        tagSet[ "tag" + sourceTag.id ] = tagInput.checked;
    }
    
    // Go through the list of images a hundred at a time to find the ones that
    // match. Wait 100ms between batches to prevent the browser from freaking
    // out at our dear user.
    this._currentFilterString = filterString;
    this._currentTagSet = tagSet;
    this._currentFilterBatch = 0;
    
    // Clear the list of displayed images and put the spinner back.
    this._emptyImageDiv.style.display = "none";
    this._imageListDiv.style.display = "";
    this._thumbnailContainer.innerHTML = "<img src='" + this._spinnerAssetPath + "' />";
    this._currentImageList = [];
    
    this.filterWorker();
};

/**
 * Updates lastFilterChange and starts the wait loop if it isn't started
 * already.
 */
NP_ImageEditorPopup.prototype.startWaiting = function( setDate ) {
    if( setDate === true ) { this._lastFilterChange = new Date(); }
    if( !this._inWaitLoop ) { this.waitToFilter(); }
};

/**
 * This function will cause the control to wait until a suitable amount of time
 * has passed and the image editor is done loading all pending images. It will
 * then begin the process of applying the current filter.
 */
NP_ImageEditorPopup.prototype.waitToFilter = function() {
    // Make sure to flag that this function is runnin'.
    this._inWaitLoop = true;
    
    // Wait some more?
    var now = new Date();
    if( !___NP_thePopup.isDoneLoading() || 
        (now.getTime() - ___NP_thePopup._lastFilterChange.getTime()) < 800 || 
        ___NP_thePopup._filtering ) {
        // Yes.
        window.setTimeout( ___NP_thePopup.waitToFilter, 100 );
    }
    else {
        ___NP_thePopup._inWaitLoop = false;
    
        // No. We can start filtering. Set a flag to make sure we don't load
        // any new pictures accidentally.
        ___NP_thePopup._filtering = true;
        ___NP_thePopup._filterTextbox.className = "imageEditorPopup_nameTextBox_filtering";
        ___NP_thePopup.filter();
    }
};

/**
 * Ensures that the pop-up DIV is visible.
 */
NP_ImageEditorPopup.prototype.show = function( sourceImage ) {
    this._mainDiv.style.display = "";
    this._sourceImage = sourceImage;
    this._altTextbox.value = sourceImage._preEditAltText;
    
    // Get a list of available images from the server.
    if( this._serverImageList.length === 0 ) {
        var service = new NeighborhoodPreviews.Services.EditService();
        
        service.GetImagePopupDataJson(
            this._sourceImage.get_loginToken(),     // Login token.
            this._sourceImage._sourceUploadImageID, // Currently selected image.
            this.imagePopupDataResponse,            // Method to call on success.
            null,                                   // Method to call on failure.
            this );                                 // Context
    }
    else {
        // Change the selected image.
        for( var i = 0; i < this._serverImageList.length; i++ ) {
            var image = this._serverImageList[ i ];
            
            if( image.imageID == this._sourceImage._sourceUploadImageID ) {
                this.changeSelectedImage( image );
            }
        }
        
        this.fixAspectRatio();
    }
};

/**
 * Hides the popup and cleans up anything it should clean up.
 */
NP_ImageEditorPopup.prototype.close = function() {
    this._mainDiv.style.display = "none";
    this._sourceImage._editing = false;
    this._sourceImage._onMouseOut( null );
};

/**
 * Saves the changes the user has made via the form.
 */
NP_ImageEditorPopup.prototype.saveChanges = function() {
    // Get the selected image ID
    var imageDBID = this._selectedImage.imageID;
    
    // Get the alt text.
    var altText = this._altTextbox.value;
    
    // Get the crop dimensions.
    var crop = this._cropRectangleDiv;
    var imageElement = this._selectedImg;
    
    var ratio = this._selectedImage.size.width / imageElement.offsetWidth;
    
    var cropX = Math.round( ratio * (crop.offsetLeft - imageElement.offsetLeft) );
    var cropY = Math.round( ratio * (crop.offsetTop - imageElement.offsetTop) );
    var cropWidth = Math.round( ratio * crop.offsetWidth );
    var cropHeight = Math.round( ratio * crop.offsetHeight );
    
    // Save changes and regenerate the image.
    var service = new NeighborhoodPreviews.Services.EditService();
    
    service.EditWebImageContent(
        this._sourceImage._loginToken,  // Login token
        this._sourceImage._dbContentID, // Web content ID
        imageDBID,                      // New image ID
        cropX,                          // New crop X
        cropY,                          // New crop Y
        cropWidth,                      // New crop width
        cropHeight,                     // New crop height
        altText,                        // New alt text,
        this.saveChangesResponse,       // Method to call on success
        null,                           // Method to call on failure
        this );                         // Context to provide to success/fail methods.
        
    // Close the editor window and update the source image to the spinner...
    this.close();
};

/**
 * Handles responses from the edit web image content service method.
 */
NP_ImageEditorPopup.prototype.saveChangesResponse = function( result, userContext, methodName ) {
    if( result === null ) {
        // Uh-oh!
        alert(
            "Sorry, but the website is currently experiencing problems, " +
            "and is unable to process your request. Please try again at " +
            "a later time." );
    }
    else {
        var newUrl = result;
        var image = $get( userContext._sourceImage._imageID );
        var caption = $get( userContext._sourceImage._captionDivID );
        var altText = userContext._altTextbox.value;
        
        image.src = newUrl + "?random=" + userContext._sourceImage._getRandomString();
        image.alt = altText;
        if( caption !== null ) { caption.innerHTML = np_encodeToHtml( altText ); }
        
        // Update the remembered data.
        userContext._sourceImage._preEditAltText = altText;
        userContext._sourceImage._sourceUploadImageID = userContext._selectedImage.imageID;
    }
};

/**
 * Starts the next set of images loading (this will be called once the user
 * scrolls to the bottom of the thumbnail list).
 */
NP_ImageEditorPopup.prototype.loadNextBatch = function() {
    var i, loadImage;
    
    // Find the first image that isn't visible.
    var startAt = null;
    for( i = 0; i < this._currentImageList.length; i++ ) {
        if( this._currentImageList[ i ].visible === false && startAt === null ) {
            // Start here.
            startAt = i;
            break;
        }
    }
    
    // Bail if there's nothing to do.
    if( startAt === null ) { return; }
    
    // Make the next batch visible.
    for( i = startAt; i < this._currentImageList.length && i < startAt + this._metaBatchSize; i++ ) {
        loadImage = this._currentImageList[ i ];
        
        // Add the image to the UI.
        loadImage.visible = true;
        this._thumbnailContainer.appendChild( loadImage.span );
    }
    
    // If we're currently downloading images, just wait for them to pick up.
    if( this._currentThumbnailLoadCount <= 0 ) {
        // Start displaying the next set of images.
        if( this._currentImageList.length < startAt + this._thumbnailLoadBatchSize ) {
            this._currentThumbnailLoadCount = this._currentImageList.length - startAt;
        }
        else {
            this._currentThumbnailLoadCount = this._thumbnailLoadBatchSize;
        }
        var loadCount = this._currentThumbnailLoadCount;
        this._lastLoadedIndex = this._currentThumbnailLoadCount - 1;

        for( var j = 0; j < loadCount; j++ ) {
            loadImage = this._currentImageList[ startAt + j ];
            
            // Create and configure a new image tag
            if( loadImage.hasImage ) {
                // This image already had a tag created for it, so let's just
                // skip it. Decrement the load count so we know not to wait 
                // for this to load.
                this._currentThumbnailLoadCount--;
            }
            else {
                var img = document.createElement( "IMG" );
                img.npImage = loadImage;
                img.style.display = "none";
                img.onload = this.imageLoaded;
                img.src = this._sourceImage.get_thumbnailRootUrl() + 
                    "?asset-path=" + encodeURIComponent( loadImage.assetPath ) + 
                    "&max-width=" + this._thumbnailSize + 
                    "&max-height=" + this._thumbnailSize;
                
                // Add the image to the document.
                loadImage.hasImage = true;
                loadImage.imgTag = img;
                loadImage.span.appendChild( img );
            }
        }
    }
};

/**
 * This function is invoked once the AJAX call to load the list of images and 
 * tags is completed. We will use it to build our internal image list and
 * create the spans, and to start the image load process.
 */
NP_ImageEditorPopup.prototype.imagePopupDataResponse = function( result, userContext, methodName ) {
    var i;

    if( result !== null && result !== undefined ) {

        // Build our JSON object.
        var evaluated = JSON.parse( result );
        var uploadImages = evaluated.uploadImages;
        var tags = evaluated.tags;
        
        // Load the image list.
        userContext._thumbnailContainer.innerHTML = "";
        userContext._serverImageList = [];
        userContext._currentImageList = [];
        
        for( i = 0; i < uploadImages.length; i++ ) {
            var image = uploadImages[ i ];
            
            image.visible = false;
            image.hasImage = false;
            image.span = document.createElement( "SPAN" );
            image.span.className = 
                (userContext._sourceImage.get_sourceUploadImageID() == image.imageID) ? 
                "imageEditorPopup_selectedThumbnail" : 
                "imageEditorPopup_thumbnail";
            image.span.innerHTML =
                "<span>" + np_encodeToHtml( image.imageName ) + "</span>" +
                "<img src='" + userContext._spinnerAssetPath + "' />";
            image.span.onclick = userContext.thumbSpanClicked;
            image.span.npImage = image;
            
            // Convert the date from a string.
            image.uploadDate = new Date( Date.parse( image.uploadDate ) );
            
            userContext._serverImageList.push( image );
            
            if( image.imageID == userContext._sourceImage._sourceUploadImageID ) {
                userContext.changeSelectedImage( image );
            }
        }
        
        // Load the tag list.
        userContext._filterTagContainer.innerHTML = "";
        for( i = 0; i < tags.length; i++ )
        {
            var tag = tags[ i ];
            
            var span = document.createElement( "SPAN" );
            span.className = "imageEditorPopup_filterTag";
            span.innerHTML = 
                "<input type='checkbox' id='imageEditorPopup_tag_" + tag.id + "' checked='checked' />" +
                "<label for='imageEditorPopup_tag_" + tag.id + "'>" +
                np_encodeToHtml( tag.name ) +
                "</label>";
                
            userContext._filterTagContainer.appendChild( span );
            
            span.childNodes[ 0 ].sourceTag = tag;
            
            userContext._tagInputs.push( span.childNodes[ 0 ] );
            
            // Initially, indicate that we've hidden the old website images.
            span.childNodes[ 0 ].checked = false;
            span.childNodes[ 0 ].onchange = ___NP_thePopup.handleTagFilter;
        }
        
        // Start the images loading.
        ___NP_thePopup._emptyImageDiv.style.display = "";
        ___NP_thePopup._imageListDiv.style.display = "none";
        
        userContext.loadNextBatch();
    }
    else {
        alert( "We're sorry, but an error prevented us from opening the " +
            "image editor for this page. This might be because you have " +
            "been idle for a long time. Try logging in again." );
    }
};

/**
 * Handles changes to the check-boxes in the tag filter list. Start the image
 * editor waiting for a filter.
 */
NP_ImageEditorPopup.prototype.handleTagFilter = function() {
    ___NP_thePopup.startWaiting( true );
};

/**
 * Handles changes to the filter text-box for image name. Start the image 
 * editor waiting for a filter.
 */
NP_ImageEditorPopup.prototype.handleTextChange = function() {
    ___NP_thePopup.startWaiting( true );
};

/**
 * Called when the user clicks on a thumbnail's name.
 */
NP_ImageEditorPopup.prototype.thumbSpanClicked = function() {
    ___NP_thePopup.changeSelectedImage( this.npImage );
};

/**
 * Changes the currently selected image.
 */
NP_ImageEditorPopup.prototype.changeSelectedImage = function( loadImage ) {
    if( this._selectedImage !== null ) {
        if( this._selectedImage.span !== null && this._selectedImage.span !== undefined ) {
            this._selectedImage.span.className = "imageEditorPopup_thumbnail";
        }
    }
    
    this._selectedImage = loadImage;

    // Change the span's class to selected.
    if( loadImage.span !== null && loadImage.span !== undefined ) {
        loadImage.span.className = "imageEditorPopup_selectedThumbnail";
    }
    
    this._selectedImg.src = this._sourceImage.get_thumbnailRootUrl() + "?asset-path=" + encodeURIComponent( loadImage.assetPath ) +
        "&max-width=" + (this._imageDiv.offsetWidth - 4) +
        "&max-height=" + (this._imageDiv.offsetHeight - 4);
        
    this._cropRectangleDiv.style.display = "";
        
    // Fix the aspect ratio.
    this._selectedImg.onload = function() {
        // Set the image to cover everything (way too much)
        ___NP_thePopup._cropRectangleDiv.style.height = "1000px";
        ___NP_thePopup._cropRectangleDiv.style.width = "1000px";
    
        // Fix the aspect ratio
        ___NP_thePopup.fixAspectRatio();
    };
};

/**
 * Ensures that the image editor's crop rectangle is currently displayed in the
 * aspect ratio desired by the site.
 */
NP_ImageEditorPopup.prototype.fixAspectRatio = function() {
    var cropDiv = this._cropRectangleDiv;
    var imageElement = this._selectedImg;
    
    // Force the aspect ratio.
    cropDiv.style.height = (Math.round(cropDiv.offsetWidth / this._sourceImage._targetAspectRatio)) + "px";
    
    // Force the fit
    if( cropDiv.offsetWidth > imageElement.offsetWidth ) {
        cropDiv.style.width = (imageElement.offsetWidth - 4) + "px";
        cropDiv.style.height = (Math.round(cropDiv.offsetWidth / this._sourceImage._targetAspectRatio) - 4) + "px";
    }
    
    if( cropDiv.offsetHeight > imageElement.offsetHeight ) {
        cropDiv.style.height = (imageElement.offsetHeight - 4) + "px";
        cropDiv.style.width = (Math.round(cropDiv.offsetHeight * this._sourceImage._targetAspectRatio) - 4) + "px";
    }
    
    // Finally, make sure the rectangle fits on the image.        
    if( cropDiv.offsetLeft < imageElement.offsetLeft ) {
        cropDiv.style.left = imageElement.offsetLeft + "px";
    }
    
    if( cropDiv.offsetTop < imageElement.offsetTop ) {
        cropDiv.style.top = imageElement.offsetTop + "px";
    }
    
    if( cropDiv.offsetLeft + cropDiv.offsetWidth > imageElement.offsetWidth + 4 ) {
        cropDiv.style.left = ((imageElement.offsetLeft + imageElement.offsetWidth) - cropDiv.offsetWidth) + "px";
    }
    
    if( cropDiv.offsetTop + cropDiv.offsetHeight > imageElement.offsetHeight + 4 ) {
        cropDiv.style.top = ((imageElement.offsetTop + imageElement.offsetHeight) - cropDiv.offsetHeight) + "px";
    }
};

/**
 * Handles mouse-down events on our crop rectangle.
 */
NP_ImageEditorPopup.prototype.cropMouseDown = function( e ) {
    // Ignore if it's not our editor.
    if( e.target !== this._cropRectangleDiv ) { return true; }
    var crop = this._cropRectangleDiv;
        
    this._cropDragging = true;
        
    this._cropDragStartClientX = e.clientX;
    this._cropDragStartClientY = e.clientY;
    this._cropDragStartOffsetX = crop.offsetLeft;
    this._cropDragStartOffsetY = crop.offsetTop;
    this._cropDragStartWidth = crop.offsetWidth;
    this._cropDragStartHeight = crop.offsetHeight;
        
    return false;
};

/**
 * Handles mouse-up events on our crop rectangle.
 */
NP_ImageEditorPopup.prototype.cropMouseUp = function( e ) {
    this._cropDragging = false;
};

/**
 * Handles mouse-move events on our crop rectangle.
 */
NP_ImageEditorPopup.prototype.cropMouseMove = function( e ) {
    var crop = this._cropRectangleDiv;
        
    if( !this._cropDragging )
    {
        // Ignore if it's not our editor.
        if( e.target !== crop ) { return; }
        
        var x = e.offsetX;
        var y = e.offsetY;
        
        if( window.navigator.appCodeName == "Mozilla" && window.navigator.vendor != "Google Inc." ) {
            y -= window.scrollY;
        }
        
        // Not currently dragging, so set it up.
        if( x <= 0 || y <= 0 )
        {
            crop.style.cursor = "nw-resize";
            this._cropDragMode = "left";
        }
        
        else if( x >= (crop.offsetWidth - 4) || y >= (crop.offsetHeight - 4) )
        {
            crop.style.cursor = "se-resize";
            this._cropDragMode = "right";
        }
        
        else
        {
            crop.style.cursor = "move";
            this._cropDragMode = "move";
        }
    }
    else
    {
        // We are currently dragging, so move/resize.
        var dx = e.clientX - this._cropDragStartClientX;
        var dy = e.clientY - this._cropDragStartClientY;
        
        if( this._cropDragMode === "move" )
        {
            crop.style.left = (this._cropDragStartOffsetX + dx) + "px";
            crop.style.top = (this._cropDragStartOffsetY + dy) + "px";
        }
        
        else if( this._cropDragMode === "right" )
        {
            if( this._cropDragStartWidth + dx >= 8 )
            {
                crop.style.width = ((this._cropDragStartWidth + dx) - 4) + "px";
            }
        }
        
        // Fix the aspect ratio.
        this.fixAspectRatio();
        
        return false;
    }
};

/**
 * Called once an image loads. Start loading the next visible image.
 */
NP_ImageEditorPopup.prototype.imageLoaded = function() {
    ___NP_thePopup._currentThumbnailLoadCount--;
    
    this.npImage.span.childNodes[ 1 ].style.display = "none";
    this.style.display = "";
    
    if( ___NP_thePopup._currentThumbnailLoadCount <= 0 ) {
        // Queue up the next batch, unless they're waiting to filter.
        if( !this._inWaitLoop ) {
            var startIndex = ___NP_thePopup._lastLoadedIndex + 1;
            var loadCount = ___NP_thePopup._thumbnailLoadBatchSize;
            if( loadCount + startIndex >= ___NP_thePopup._currentImageList.length ) {
                loadCount = ___NP_thePopup._currentImageList.length - startIndex;
            }
            
            ___NP_thePopup._currentThumbnailLoadCount = loadCount;
            ___NP_thePopup._lastLoadedIndex = ___NP_thePopup._lastLoadedIndex + loadCount;
            
            for( var i = 0; i < loadCount; i++ ) {
                var loadImage = ___NP_thePopup._currentImageList[ startIndex + i ];
                
                // Make sure we're supposed to load this.
                if( loadImage.visible === true ) {
                    if( loadImage.hasImage ) {
                        // This image was already loaded. We won't create a new
                        // image tag, but make sure that we don't wait for it to
                        // load forever.
                        ___NP_thePopup._currentThumbnailLoadCount--;
                    }
                    else {
                        // Create and configure a new image tag
                        var img = document.createElement( "IMG" );
                        img.npImage = loadImage;
                        img.style.display = "none";
                        img.onload = ___NP_thePopup.imageLoaded;
                        img.src = ___NP_thePopup._sourceImage.get_thumbnailRootUrl() + "?asset-path=" + encodeURIComponent( loadImage.assetPath ) + "&max-width=" + ___NP_thePopup._thumbnailSize + "&max-height=" + ___NP_thePopup._thumbnailSize;
                        
                        // Add the image to the document.
                        loadImage.hasImage = true;
                        loadImage.imgTag = img;
                        loadImage.span.appendChild( img );
                    }
                }
                else {
                    // Short-circuit!
                    ___NP_thePopup._currentThumbnailLoadCount -= (loadCount - i);
                    ___NP_thePopup._lastLoadedIndex = (startIndex + i) - 1;
                    
                    return;
                }
            }
        }
    }
};

// The constructor for this control.
Website.DynamicImageContentControl = function( element ) 
{
    // Initialize some of our variables.
    this._dbContentID = -1;
    this._sourceUploadImageID = -1;
    this._allowEdit = false;
    this._loginToken = null;
    this._targetAspectRatio = 1.0;
    this._thumbnailRootUrl = "";
    this._spinnerAssetPath = "";
    this._preEditAltText = "";
    
    this._editButtonID = "";
    this._imageID = "";
    this._displayDivID = "";
    this._captionDivID = "";
    
    // Required event handlers.
    this._mouseOverHandler = null;
    this._mouseOutHandler = null;
    this._clickHandler = null;
    
    // Control state.
    this._editing = false;
    this._originalBorder = null;

    // Call the base class' constructor.
    Website.DynamicImageContentControl.initializeBase(this, [element]);
};

// The control's prototype.
Website.DynamicImageContentControl.prototype = 
{
    // Get and set for login token.
    get_loginToken : function()
    {
        return this._loginToken;
    },
    
    set_loginToken : function( value )
    {
        this._loginToken = value;
    },
    
    // Get and set for spinner asset path.
    get_spinnerAssetPath : function()
    {
        return this._spinnerAssetPath;
    },
    
    set_spinnerAssetPath : function( value )
    {
        this._spinnerAssetPath = value;
    },
    
    // Get and set for DB content ID.
    get_dbContentID : function()
    {
        return this._dbContentID;
    },
    
    set_dbContentID : function( value )
    {
        this._dbContentID = value;
    },
    
    // Get and set for edit button ID.
    get_editButtonID : function()
    {
        return this._editButtonID;
    },
    
    set_editButtonID : function( value )
    {
        this._editButtonID = value;
    },
    
    // Get and set for edit button ID.
    get_displayDivID : function()
    {
        return this._displayDivID;
    },
    
    set_displayDivID : function( value )
    {
        this._displayDivID = value;
    },
    
    // Get and set for edit button ID.
    get_imageID : function()
    {
        return this._imageID;
    },
    
    set_imageID : function( value )
    {
        this._imageID = value;
    },
    
    // Get and set for allow edit
    get_allowEdit : function()
    {
        return this._allowEdit;
    },
    
    set_allowEdit : function( value )
    {
        this._allowEdit = value;
    },
    
    // Get and set for the source upload image ID.
    get_sourceUploadImageID : function()
    {
        return this._sourceUploadImageID;
    },
    
    set_sourceUploadImageID : function( value )
    {
        this._sourceUploadImageID = value;
    },
    
    // Get and set for the thumbnail root URL.
    get_thumbnailRootUrl : function()
    {
        return this._thumbnailRootUrl;
    },
    
    set_thumbnailRootUrl : function( value )
    {
        this._thumbnailRootUrl = value;
    },
    
    // Get and set for the target aspect ratio.
    get_targetAspectRatio : function()
    {
        return this._targetAspectRatio;
    },
    
    set_targetAspectRatio : function( value )
    {
        this._targetAspectRatio = value;
    },
    
    // Get and set for caption div ID
    get_captionDivID : function()
    {
        return this._captionDivID;
    },
    
    set_captionDivID : function( value )
    {
        this._captionDivID = value;
    },
    
    /* Property Template
    // Get and set for
    get_ : function()
    {
        return this._;
    },
    
    set_ : function( value )
    {
        this._ = value;
    },
    */

    // Initializes the control.
    initialize: function() 
    {
        // Let the base class initialize.
        Website.DynamicImageContentControl.callBaseMethod(this, 'initialize');
        
        var target = this.get_element();
        
        // Remember some initial settings.
        this._originalBorder = target.style.border;
        
        // Attach to events.
        if( this._allowEdit )
        {
            this._mouseOverHandler = Function.createDelegate( this, this._onMouseOver );
            this._mouseOutHandler = Function.createDelegate( this, this._onMouseOut );
            this._clickHandler = Function.createDelegate( this, this._onClick );
            
            $addHandlers(
                target,
                {
                    'mouseover':    this._mouseOverHandler,
                    'mouseout':     this._mouseOutHandler,
                    'click':        this._clickHandler
                },
                this
                );
        }
    },
    
    // Cleans up the control.
    dispose: function() 
    {      
        if( this._allowEdit )
        {  
            // Detatch all event handlers.
            $clearHandlers( this.get_element() );
            
            delete this._mouseOverHandler;
            delete this._mouseOutHandler;
            delete this._clickHandler;
        
            // Let the base class shut down.
            Website.DynamicImageContentControl.callBaseMethod(this, 'dispose');
        }
    },
    
    // Positions the edit button.
    positionEditButton : function()
    {
        var editButton = $get( this._editButtonID );
        var display = $get( this._displayDivID );
        
        var buttonWidth = editButton.offsetWidth;
        var buttonHeight = editButton.offsetHeight;
        
        var displayWidth = display.offsetWidth;
        var displayHeight = display.offsetHeight;
        
        editButton.style.left = (displayWidth/2 - buttonWidth/2) + "px";
        editButton.style.top = (displayHeight/2 - buttonHeight/2) + "px";
    },
    
    // Provides some mouse-over and mouse-out handling logic.
    _onMouseOver : function( e )
    {
        if( !this._editing )
        {
            this.get_element().style.border = '2px dotted red';
            
            $get( this._editButtonID ).style.display = "";
            this.positionEditButton();
        }
    },
    
    _onMouseOut : function( e )
    {
        if( !this._editing )
        {
            this.get_element().style.border = this._originalBorder;
            $get( this._editButtonID ).style.display = "none";
        }
    },
    
    // Handles button clicks.
    _onClick : function( e )
    {
        var service, caption;
        
        if( e.target === $get( this._editButtonID ) )
        {
            // Enter edit mode.
            this._editing = true;
            this._preEditAltText = $get( this._imageID ).alt;
            
            // Test code for da pop-up...
            if( ___NP_thePopup === null ) {
                ___NP_thePopup = new NP_ImageEditorPopup( this._spinnerAssetPath );
            }
            ___NP_thePopup.show( this );
        }
    },
    
    // Returns a string of 20 random digits.
    _getRandomString : function()
    {
        var output = "";
        var i = 0;
        
        for( i = 0; i < 20; i++ )
        {
            output += Math.round( Math.random() * 9 );
        }
            
        return output;
    }
};

// Register the control.
Website.DynamicImageContentControl.registerClass('Website.DynamicImageContentControl', Sys.UI.Control);
if (typeof(Sys) !== 'undefined') { Sys.Application.notifyScriptLoaded(); }

