/**
 * JavaScript needed for FeedMagnet embedding.
 *   author: Luke Sneeringer
 *   copyright: (C) 2010, FeedMagnet
 */
// Base init file. The big thing this provides is FM.include and FM.ready.
// FM.include is used to add something to the FM namespace. FM.ready is used
// to delay execution of a function until any necessary scaffolding things
// have been pulled in.
(function() {
    // sanity check: this doesn't need to be run again if it's
    // already been run once...
    if (window.FM) {
        return
    }
    
    // Attempt to keep everything tidily in a namespace...
    window.FM = {
        // a FeedMagnet-specific "on ready" function -- this will delay execution of
        // the function passed to it until required scaffolding (right now, jQuery)
        // has been loaded
        'ready': function(fx) {
            if (FM._ready === true) {
                return fx(FM.$)
            }
            window.setTimeout(function() { FM.ready(fx) }, 100)
        },
        
        
        // retrieve jQuery for use...
        '_load_jquery': function(url) {
            // is jQuery already loaded? if so, there's no
            // reason to load it again
            if (typeof jQuery !== 'undefined' && jQuery.fn.jquery >= '1.4') {
                FM.$ = jQuery
                jQuery(document).ready(function() {
                    FM._ready = true
                })
                return true
            }
            
            // create a script tag that points to the remote URL
            var script = document.createElement('script')
            script.type = 'text/javascript'
            script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'
            
            // append the script tag to the <head /> tag of this document
            document.getElementsByTagName('head')[0].appendChild(script)
            
            // once jQuery is loaded, set it in no conflict mode
            var cont = window.setInterval(function() {
                if (typeof jQuery !== 'undefined' && jQuery === $) {
                    // loading finished; I don't need to keep doing this
                    window.clearInterval(cont)
                                        
                    // be responsible -- don't trample on the global namespace
                    // for other people, especially with something like $ that other
                    // tools use
                    FM.$ = jQuery.noConflict(true)
                    
                    // state that everything is ready (assuming the DOM has loaded also)
                    FM.$(document).ready(function() {
                        FM._ready = true
                    })
                }
            }, 50)
        },
        
        
        // load an object into the FM namespace
        // basically the same as FM[namespace] = { ... }, but
        // with a couple of extra overwrite checks (and a place for
        // me to add universal functionality later if I need it)
        'include': function(namespace, object, overwrite) {
            // init vars
            overwrite = overwrite || false
            
            // sanity check: right now, I expect a string here
            // FIXME: Make this take an already instantiated object in FM.*
            if (typeof namespace !== 'string') {
                throw new TypeError('Expected namespace to be a string.')
            }
            
            // parse out the namespace and ensure each necessary level
            // in the namespace exists
            var namespace_list = namespace.split('.')
            var cursor = FM
            
            // if there is more than one level being added here,
            // make sure each level before the last one in the tree
            // both exists and is an object, creating levels that don't
            // exist.
            // raises TypeError if I find something I did not expect
            for (var i = 0; i < namespace_list.length - 1; i += 1) {
                var ns = namespace_list[i]
                if (typeof cursor[ns] === 'undefined') {
                    cursor[ns] = {}
                }
                else if (typeof cursor[ns] !== 'object') {
                    throw new TypeError('Found something unexpected in the namespace tree.')
                }
                
                // advance the cursor
                cursor = cursor[ns]
            }
            
            // the last item in the namespace is where the object will
            // live, set it to null for now unless it already exists
            var key = namespace_list[namespace_list.length - 1]
            if (typeof cursor[key] === 'undefined') {
                cursor[key] = null
            }
            
            // if the object is not an actual object, then just write it to
            // cursor[key] and be done
            if (typeof object !== 'object') {
                if (cursor[key] === null || overwrite === true) {
                    cursor[key] = object
                    return
                }
                else {
                    throw new IndexError('Tried to write a non-object (function, string, etc.) to the namespace, but something is already present.')
                }
            }
            
            // if the object is an actual object, iterate over the keys and
            // set them to cursor[key] -- but be nice about it :)
            if (cursor[key] === null) {
                cursor[key] = {}
            }
            for (var k in object) {
                if (typeof cursor[key][k] === 'undefined' || overwrite === true) {
                    cursor[key][k] = object[k]
                }
                else {
                    throw new IndexError('Tried to write to a location that already exists.')
                }
            }
        },
        
        // variables
        'cache': {},
        '_ready': false,
        '_gradual_display_running': false,  // FIXME: move this
        
        // constants
        'HOSTNAME': 'http://www.feedmagnet.com/',
        'POLL_TIME': 30,
        'LOG_LEVEL': 'WARNING',
        'VERSION': 1.0
    }
    
    FM._load_jquery()
})();
                
// convert all flowplayer videos
// flowplayer('a.flowplayer-video', '/m/swf/flowplayer-3.1.5.swf')
// perform all the random stuff (mostly setting .live on classes) to make
// magnets work, and be awesome
FM.ready(function($) {
    // make the new posts button or buttons work
    $('.__new-posts__').live('click', function() {
        $(this).parents('.__bucket__').find('.__update__:hidden').slideDown()
        $(this).slideUp()
    })

    // if I have reject buttons, make them work correctly
    $('.__reject__').live('click', function() {
        // get the update being pushed aside
        var update = $(this).parents('.__update__')
        var id = update.find('.__id__').html()

        $.ajax({
            'url': FM.HOSTNAME + 'moderate/' + id + '/delete/',
            'type': FM.util.method('DELETE'),
            'dataType': FM.util.data_type(),
            'success': function() {
                update.slideUp('normal', function() {
                    $(this).remove()
                })
            }
        })
    })


    // if I have blacklist buttons, make them function
    $('.__blacklist__').live('click', function() {
        // first, what is the author and site
        var author = $(this).parents('.__update__').find('.__author__').text()
        var site = $(this).parents('.__update__').find('.__site__').text()

        // give a confirmation message
        if (!window.confirm('Are you sure you want to blacklist ' + author + ' on ' + site + '? This will prevent any updates from this author from appearing in your feed.')) {
            return false
        }

        // tell the server to blacklist this person for this account
        $.ajax({
            'url': FM.HOSTNAME + 'blacklisting/author/',
            'type': FM.util.method(),
            'dataType': FM.util.data_type(),
            'data': {
                'author': author,
                'site': site
            },
            'success': function() {
                // find all updates with this author and site and remove them
                $('.__update__').each(function() {
                    if (author == $(this).find('.__author__').text() && site == $(this).find('.__site__').text()) {
                        $(this).slideUp('normal', function() {
                            $(this).remove()
                        })
                    }
                })
            }
        })
    })
    
    // if there are scrolling arrows, make them work
    var scroll_block = function($el, direction) {
        // get elements I need
        var $section = $el.parents('.__scroll-section__')
        var $window = $section.find('.__scroll-window__')
        var $content = $section.find('.__scroll-content__')

        // what is the height of the content block
        // (which is the amount I will scroll)?
        var height = parseInt($window.innerHeight())

        // which way am I going
        if (direction === 'down') {
            height *= -1
        }

        // what is the current offset?
        var current_offset = parseInt($content.css('margin-top'))
        var target_offset = current_offset + height

        // sanity check: the target offset should never go above 0
        if (target_offset > 0) {
            target_offset = 0
        }

        // sanity check: am I about to scroll the last item off the screen?
        // I need to never have a number that is less than the beginning of
        // the starting element
        var $last = $content.children(':last')
        var oops = $last.position().top + $last.innerHeight()
        if (target_offset < -oops) {
            return null
        }

        // animate the change
        $content.animate({
            'margin-top': target_offset + 'px'
        }, 'fast')
    }

    // actually invoke the correct function on the arrow classes
    $('.__scroll-up-arrow__').live('click', function() {
        scroll_block($(this), 'up')
    })
    $('.__scroll-down-arrow__').live('click', function() {
        scroll_block($(this), 'down')
    })
});
// magnet functions for the FeedMagnet embeddeding system, including
// functions to load a magnet from the server
FM.include('magnets', {
    // load a magnet, and get the initial ball rolling
    'load': function(slug, kwargs) {
        var $ = FM.$
        kwargs = kwargs || {}
        kwargs.locations = kwargs.locations || {}
        kwargs.callback = kwargs.callback || null
        
        // load the CSS for the magnet
        FM.util.log('Loading magnet CSS.', 'DEBUG')
        if (FM.util.local() === false) {
            FM.util.css(FM.HOSTNAME + 'embed/' + slug + '.css')
        }
        
        // backwards compatibility: is "locations" a string? if so,
        // assume it's the location for everything (and only expect to get one thing)
        if (typeof kwargs.locations === 'string') {
            FM.util.log('Location was sent as a string (backwards compatbility mode).', 'INFO')
            kwargs.locations = { '__all__': kwargs.locations }
        }
    
        // resolve any locations sent as a string or a DOM object to a jQuery object
        for (var i in kwargs.locations) {
            var location = $(kwargs.locations[i])
        
            // backwards compatibility: if this resolves to something with .length == 0,
            // then perhaps it was sent as an ID?
            if (location.length === 0) {
                var location = $('#' + kwargs.locations[i])
            }
        
            // add the jQuery object to the locations dictionary
            FM.util.log('Location length: ' + location.length, 'DEBUG')
            kwargs.locations[i] = location
        }
    
        // send a request to the server 
        $.ajax({
            'url': FM.HOSTNAME + slug + '/',
            'dataType': FM.util.data_type(),
            'type': FM.util.method(),
            'success': function(response) {
                // first of all, overwrite the current contents of each
                // location for which I got data with the outside bucket div
                for (var bucket_name in response) {
                    // do I have a location for this bucket?
                    if (typeof kwargs.locations[bucket_name] !== 'undefined') {
                        var key = bucket_name
                    }
                    else if (typeof kwargs.locations['__all__'] !== 'undefined') {
                        var key = '__all__'
                    }
                    else {
                        continue
                    }
                    
                    // do the initial setup for the bucket
                    FM.util.log('Magnet loaded. Proceeding to bucket setup.', 'DEBUG')
                    FM.magnets.setup_bucket(slug, bucket_name, response[bucket_name].options, kwargs.locations[key])                    
                }
                
                // done making the buckets; process the response as usual
                FM.util.log('Processing the initial feed from the FeedMagnet server.', 'DEBUG')
                FM.magnets.process_response(response, 'show')
                
                // now the bucket is completely loaded; run the post-load hook
                // for each bucket
                for (var bucket_name in response) {
                    FM.magnets.on_load(bucket_name, response[bucket_name]['options'])
                }
                
                // begin polling
                FM.magnets.poll(slug, false, kwargs.callback)
            }
        })
    },
    
    
    // do the initial setup for a bucket
    'setup_bucket': function(slug, bucket_name, bucket_options, $location) {
        // jQuery!
        var $ = FM.$
        
        // scrap everything in the location now
        // (likely a "loading..." graphic or something similar)
        $location.empty()
        
        // add the outer bucket div
        var $bucket = $('<div id="__bucket-' + bucket_name + '__" class="__bucket__ __bucket-' + bucket_options.type + '__">')
        
        // add the new posts div and the load more div;
        if (bucket_options.feed) {
            $('<div class="__new-posts__"></div>').html('we\'ve pulled in <em>0</em> new posts since you loaded the page...click to view...').appendTo($bucket)
            var $lm = $('<div class="__load-more__">load more...</div>').appendTo($bucket)
        }
        
        // place the $bucket div on the DOM
        $bucket.appendTo($location)
        FM.util.log('Bucket DIV created and placed in the DOM.', 'DEBUG')
        
        // make the load more button or buttons work
        if (typeof $lm !== 'undefined') {
            $lm.click(function() {
                FM.magnets.more(slug, $bucket)
            })
        }
    },
    
    
    // do any setup that must be done after a magnet has loaded;
    // this method will run once per magnet, once the magnet's initial load is complete
    'on_load': function(bucket_name, options) {
        var $ = FM.$
        var $bucket = $('#__bucket-' + bucket_name + '__')
        
        // if there are pretty times, make them pretty :)
        FM.magnets.populate_pretty_times($bucket)
        
        // if there is a ticker, make it function
        if (typeof options.loop !== 'undefined') {
            // init vars
            var updates = $bucket.find('.__update__')
            var limit = options.limit || 5
            var type = options.loop || 'cycle'
            var refresh = options.refresh || 6.0
            var stale = options.stale || 0
            var animation = options.anim || 'default'
            
            // sanity check
            if (limit >= 20) {
                limit = 5
            }

            // run the stale worker for the first time
            FM.magnets.looping._deal_with_staleness(updates, stale)

            // hide all of the updates within the ticker to begin with,
            // except show the last n (where n == limit)
            updates.slice(0, -limit).hide()  // ideally this is done in CSS and does nothing, but jic...
            updates.filter(':not(.__stale__)').slice(-limit).show()

            // was an animation style set? if it's set to default, I need to
            // determine which that is based on the loop type
            if (animation != 'slide' && animation != 'swipe' && animation != 'fade') {
                animation = 'fade'
                if (type == 'roll') {
                    animation = 'slide'
                }
            }

            // what is the animation function being used?
            var anim = { 'type': animation }
            switch (animation) {
                case 'slide':
                    anim.show = 'slideDown'
                    anim.hide = 'slideUp'
                    break
                case 'swipe':
                    anim.show = 'show'
                    anim.hide = 'hide'
                    break
                case 'fade':
                    anim.show = 'fadeIn'
                    anim.hide = 'fadeOut'
                    break
            }

            // every six seconds, I want to show new updates, moving down to
            // the bottom of the list once I get to the top
            var rotate = function() {
                // make sure my list of updates is current
                updates = $bucket.find('.__update__')

                // first, run a staleness check
                FM.magnets.looping._deal_with_staleness(updates, stale)

                // get the list of updates I want to show next, and the list of
                // updates I want to hide; this changes based on the loop type
                if (typeof FM.magnets.looping[type] === 'function') {
                    FM.magnets.looping[type]($bucket.find('.__update__'), limit, anim)
                }
                else {
                    // the loop rule that the user specified isn't something that
                    // I have a function for in FM.magnets.looping; abort
                    return null
                }

                // do this again in "var refresh" seconds...
                window.setTimeout(rotate, refresh * 1000)
            }

            // kick off the first one in "var refresh" seconds after load
            window.setTimeout(rotate, refresh * 1000)
        }
    },


    // return the update IDs in each bucket as a dictionary
    'get_existing': function() {
        // declare vars
        var $ = FM.$
        var answer = []

        // iterate over each bucket
        $('.__bucket__').each(function() {
            // the answer for this bucket
            var bucket_answer = []

            // iterate over the updates in the bucket and add all of the
            // site-tpid sets that I have
            $(this).find('.__update__').each(function() {
                var uuid = $(this).attr('id').slice(9, -2)
                bucket_answer.push(uuid)
            })

            // only return the most recent 20
            if (bucket_answer.length > 20) {
                bucket_answer = bucket_answer.slice(0, 20)
            }

            // append the bucket answer to the ultimate answer
            answer = answer.concat(bucket_answer)
        })

        return answer
    },


    // poll the server for new updates; if there are no new updates,
    // wait a little longer before polling the server again
    'poll': function(slug, wait, callback) {
        var $ = FM.$
        wait = wait || FM.POLL_TIME
        callback = callback || null

        var _do = function() {    
            // gather my POST parameters
            var data = {}

            // only send current-updates information on
            // local requests; it's too big for JSONP
            if (FM.util.local() === true) {
                data['current-updates'] = FM.magnets.get_existing()
            }
            
            // get my oldest update in each bucket
            $('.__bucket__').each(function() {
                var bucket_name = $(this).attr('id').slice(9, -2)
                var updates = $(this).find('.__update__')
                if (updates.length > 20) {
                    var update = updates.eq(19)
                }
                else {
                    var update = updates.eq(updates.length - 1)
                }

                // get the timestamp of the update I am using
                data[bucket_name + '-oldest'] = update.find('.__unix-timestamp__').text()
            })
            
            // retrieve updates
            $.ajax({
                'url': FM.util.url(slug),
                'data': $.param(data, true),  // extra "true" makes it do the POST standard and not the PHP/Rails way
                'dataType': FM.util.data_type(),
                'type': FM.util.method(),
                'success': function(response, status) {
                    // process the response and add any new updates
                    // to the document
                    FM.magnets.process_response(response)
                }
            })
        }

        // run the poll as soon as it's time...
        var interval = window.setInterval(_do, wait * 1000)
        
        // I may be passed a callback to deal with the poller (so it's not
        // just a fire and forget); run it...
        if (typeof callback === 'function') {
            callback(interval)
        }
        
        return true
    },


    // load more updates at the bottom of a feed
    // (this is a response to a user request, generally)
    'more': function(slug, $bucket) {
        var $ = FM.$
        
        // get the bucket name as a string
        var bucket_name = $bucket.attr('id').slice(9, -2)

        // get the last item's timestamp
        var post = {}
        var timestamp = null
        $bucket.find('.__update__').each(function() {
            var update_ts = parseInt($(this).find('.__unix-timestamp__').text())
            if (timestamp === null || update_ts < timestamp) {
                timestamp = update_ts
            }
        })

        // create my data dictionary
        post[bucket_name + '-oldest'] = timestamp
        post['load-more'] = bucket_name
        
        // actually send the request
        $.ajax({
            'url': FM.util.url(slug),
            'data': $.param(post, true),
            'type': FM.util.method(),
            'dataType': FM.util.data_type(),
            'success': function(response, status) {
                // add the response to the document body
                FM.magnets.process_response(response, 'show')
            }
        })
    },


    // process a response and place the elements
    // into the DOM structure
    'process_response': function(response, override_display) {
        // init vars
        var $ = FM.$
        override_display = override_display || null
        
        // loop over each bucket and process each one
        // individually
        for (var bucket in response) {
            // get the bucket in the DOM
            var $bucket = $('#__bucket-' + bucket + '__')
            var options = response[bucket].options
            
            // determine the appropriate display mechanism for this bucket
            var display = response[bucket]['options']['poll_display'] || 'show'
            if (override_display !== null) {
                display = override_display
            }
            FM.util.log('Processing a FeedMagnet response. Display is set to ' + display, 'DEBUG')

            // iterate over each new update in the bucket
            FM.util.log('Found ' + response[bucket].updates.length + ' updates. Processing.', 'DEBUG')
            var updates_processed = 0
            for (var i = 0; i < response[bucket]['updates'].length; i += 1) {
                FM.util.log('Beginning work on update ' + i + '.', 'DEBUG')
                var update = $(response[bucket]['updates'][i])
                
                // sanity check: do I already have this update?
                var update_id = update.attr('id')
                if ($bucket.find('#' + update_id).length > 0) {
                    continue
                }
                
                // sanity check: have I processed too many updates?
                if (options.limit && updates_processed >= options.limit) {
                    FM.util.log('Skipping this update because it\'s over the limit.', 'DEBUG')
                    continue
                }

                // when this update actually goes in, it's going to be hidden
                update.css('display', 'none')

                // does this bucket have discrete dates?
                var discrete_dates = ($bucket.find('.__discrete-date__').length > 0)

                if (discrete_dates) {
                    // it does -- find the **correct** discrete date
                    var my_date = parseInt(update.find('.__ymd__').text())
                    var dd = $bucket.find('#__date-' + my_date + '__')

                    // sanity check: if I didn't find that date...
                    if (dd.length == 0) {
                        // make a new div for that date
                        dd = $('<div id="__date-' + my_date + '__" class="__discrete-date__" />')

                        // insert it in the correct place
                        var date_after = null
                        var discrete_dates = $bucket.find('.__discrete-date__')
                        discrete_dates.each(function() {
                            var its_date = parseInt($(this).attr('id').substr(7, 8))
                            if (date_after === null && my_date > its_date) {
                                date_after = $(this)
                            }
                        })

                        if (date_after) {
                            dd.insertBefore(date_after)
                        }
                        else {
                            dd.insertAfter(discrete_dates.eq(discrete_dates.length - 1))
                        }

                        // now I can just insert the update and be done; I don't need
                        // to do any more processing
                        update.appendTo(dd)
                        continue
                    }

                    // I only want to look at updates within this particular discrete date
                    var scope = dd
                    var updates = dd.find('.__update__')
                }
                else {
                    FM.util.log('No discrete dates.', 'DEBUG')
                    var updates = $bucket.find('.__update__')
                }

                // it's new; now where do I put it?
                // iterate over the existing updates and find the two that it fits between
                FM.util.log('Placing update ' + i + '.', 'DEBUG')
                var my_timestamp = parseInt(update.find('.__unix-timestamp__').text())
                var after = null
                for (var j = 0; j < updates.length; j += 1) {
                    var its_timestamp = parseInt(updates.eq(j).find('.__unix-timestamp__').text())

                    // if the timezone of the item I'm checking is less than mine,
                    // then I've found the first item that belongs __after__ mine
                    if (its_timestamp < my_timestamp) {
                        after = updates.eq(j)
                        break
                    }
                }
                
                // does this update have a spot?
                if (after) {
                    update.insertBefore(after)
                }
                else {
                    // this item belongs at the end of the list
                    if (updates.length > 0) {
                        update.insertAfter(updates.eq(updates.length - 1))
                    }
                    else if (scope) {
                        update.appendTo(scope)
                    }
                    else if ($bucket.find('.__load-more__').length > 0) {
                        update.insertBefore($bucket.find('.__load-more__'))
                    }
                    else {
                        // fuck it
                        update.appendTo($bucket)
                    }
                }
                
                // denote that the update is processed
                updates_processed += 1

                // if the update has a pretty time, make sure it's displayed as such
                update.find('.__prettytime__').each(function() {
                    $(this).text(FM.util.pretty_time(update.find('.__unix-timestamp__').text()))
                })
            }
            FM.util.log('Finished processing updates', 'DEBUG')

            // if this item isn't supposed to be hidden initially,
            // show it now
            // items should be hidden if the magnet display option is set to hide
            // **or** if the update is part of a segment that is looping
            if (display === 'hide' || typeof options.loop !== 'undefined') {
                // check to see if there are changes and if there are, display
                // the user control allowing the user to see them
                var new_updates = $bucket.find('.__update__:hidden')
                if (new_updates.length > 0 && typeof options.loop === 'undefined') {
                    $bucket.find('.__new-posts__ em').text(new_updates.length.toString())
                    $bucket.find('.__new-posts__').slideDown()
                }
            }
            else if (display === 'gradual') {
                FM.magnets.gradual_display($bucket)
            }
            else {
                FM.util.log('Display is set to show and there is no loop. Showing all updates.', 'DEBUG')
                $('.__update__:hidden').slideDown()
            }
        }
    },


    // function to gradually display hidden updates that have come in
    'gradual_display': function($bucket, ignore_concurrency_check) {
        // init vars
        var $ = FM.$
        ignore_concurrency_check = ignore_concurrency_check || false

        // is this method already running? if so, don't start it again
        if (FM._graudal_display_running === true && ignore_concurrency_check !== true) {
            return null
        }

        // wrapper for FM.gradual_display to get setTimeout (below) to behave
        var _stGradualDisplay = function() {
            return FM.magnets.gradual_display($bucket, true)
        }

        // denote that the method is running
        FM._gradual_display_running = true

        // sanity check: has the list of hidden items grown
        // too large? if so, knock off the older ones...
        if ($bucket.find('.__update__:hidden').length > 15) {
            $bucket.find('.__update__:hidden').slice(5).remove()
        }

        // sanity check: has the list of visible items grown
        // too large? if so, knock off the really old ones...
        if ($bucket.find('.__update__:visible').length > 100) {
            $bucket.find('.__update__:visible').slice(75).remove()
        }

        // find the item to be shown
        var item = $bucket.find('.__update__:visible').eq(0).prevAll('.__update__:hidden').eq(0)  // note that prevAll reverses their DOM order
        if (item.length === 1) {
            // set time to wait before showing next item (30s / number of updates left)
            var wait_time = 1000 * ((FM.POLL_TIME * .75) / ($bucket.find('.__update__:hidden').length + 1))
            // actually show the new item, and rerun this method
            // to get the next one
            item.slideDown()
            window.setTimeout(_stGradualDisplay, wait_time)
        }
        else {
            // I'm done -- and therefore I am not running anymore;
            // so long, and thanks for all the fish!
            FM._gradual_display_running = false
        }
    },


    // method to change times into a "pretty" format (e.g. "yesterday"; "3 days ago";
    // "just now") and maintain their accuracy
    'populate_pretty_times': function($bucket) {
        var $ = FM.$
        $bucket.find('.__prettytime__').each(function() {
            var unix_timestamp = parseInt($(this).parents('.__update__').find('.__unix-timestamp__').text())
            $(this).text(FM.util.pretty_time(unix_timestamp))
        })
        window.setTimeout(function() {
            FM.magnets.populate_pretty_times($bucket)
        }, 5000)
    }
});

// define the overall feedmagnet() function -- in the global namespace -- that we will offer
// to be the one-line magnet load.
function feedmagnet(slug, kwargs) {
    var options = options || {}
    options.locations = kwargs.locations || kwargs
    options.callback = kwargs.callback || null
    
    // if there is a __dummy__ key, remove it;
    // this is getting around an annoyance between the juxtaposition
    // of Django template limitations and JavaScript syntax
    if (typeof options.locations.__dummy__ !== 'undefined') {
        delete options.locations.__dummy__
    }
    
    // okay, now do our thing...
    FM.ready(function($) {
        // load the magnet itself
        FM.magnets.load(slug, options)
    })
};
// general utility for the FeedMagnet embedding system
FM.include('util', {
    // utility function to generate a probably-unique identifier
    'uuid': function() {
        return (Math.random() * Math.pow(2, 31)).toString(36)
    },


    // weak function to perform a browser sniff, and cache
    // the result
    'browser': function() {
        // the browser has not been determined; figure it out
        // and cache the result to the FM object
        var ua = navigator.userAgent.toLowerCase()
        if (ua.indexOf('khtml') !== -1) {
            return 'webkit'
        }
        else if (ua.indexOf('msie') !== -1) {
            return 'msie'
        }

        // return the now-cached result
        return 'gecko'
    },


    // given a Unix timestamp, return a string representing a
    // "pretty time" (e.g. "just now", "10 minutes ago", etc.)
    'pretty_time': function(unix_timestamp) {
        // init vars
        var date = new Date(parseInt(unix_timestamp) * 1000)
        var now = new Date()
        var delta = (now.getTime() - date.getTime()) / 1000

        // sanity check: is delta a negative? I don't have a mechanism for addressing
        // dates that are after now...
        if (delta < 0) {
            return null
        }

        // sanity check: is delta more than 3 weeks ago? if so, just send
        // back an absolute date rather than a relative one...
        if (Math.ceil(delta / 86400) > 21) {
            return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][date.getMonth()] + ' ' +
                date.getDate() + ', ' +
                date.getFullYear()
        }

        // return the appropriate block of text
        if (delta < 60) {
            return 'just now'
        }
        else if (delta < 120) {
            return '1 minute ago'
        }
        else if (delta < 3600) {
            return Math.floor(delta / 60) + ' minutes ago'
        }
        else if (delta < 7200) {
            return '1 hour ago'
        }
        else if (delta < 86400) {
            return Math.floor(delta / 3600) + ' hours ago'
        }
        else if (Math.floor(delta / 86400) == 1) {
            return 'yesterday'
        }
        else if (Math.floor(delta / 86400) <= 7) {
            return Math.floor(delta / 86400) + ' days ago'
        }
        else {
            return Math.ceil(delta / (86400 * 7)) + ' weeks ago'
        }
    },
    
    
    // determine whether or not FeedMagnet is being run locally
    // from feedmagnet.com, or being injected
    'local': function() {
        return 'http://' + window.location.hostname + '/' == FM.HOSTNAME
    },
    
    
    // get the full URL to use when given a slug
    'url': function(slug) {
        // get the full URL to poll
        var url = FM.HOSTNAME + slug + '/'
        if (FM.util.local() === true) {
            url += window.location.search
        }
        return url
    },
    
    
    // if requests are local, receive JSON replies; if they are
    // remote, use JSONP
    'data_type': function() {
        if (FM.util.local() === true) {
            return 'json'
        }
        return 'jsonp'
    },
    
    
    // if requests are local, use primarily POST requests (or anything
    // else specified by the argument); for remote, always use GET
    'method': function(method_) {
        method_ = method_ || 'POST'
        if (FM.util.local() === true) {
            return method_
        }
        return 'GET'
    },
    
    
    // load a remote CSS file via. the "jsonp" methodology
    'css': function(url) {
        // create a link tag that points to the remote URL
        var link = document.createElement('link')
        link.rel = 'stylesheet'
        link.type = 'text/css'
        link.href = url

        // append the link tag to the <head /> tag of this document
        document.getElementsByTagName('head')[0].appendChild(link)
    },
    
    
    // search an array to find the index of the given item;
    // return the index if found or -1 otherwise
    'index': function(haystack, needle) {
        // iterate over the haystack looking for the needle
        for (var i = 0; i < haystack.length; i += 1) {
            if (haystack[i] === needle) {
                return i
            }
        }
        return -1
    },
    
    
    // log something based on the current log level
    'log': function(message, level) {
        // what are my potential error levels
        var levels = ['OFF', 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
        level = level.toUpperCase()
        
        // sanity check: is this a level I know?
        if (FM.util.index(levels, level) === -1) {
            return false
        }
        
        // is this level greater than my log level? if so, stop.
        if (FM.util.index(levels, level) > FM.util.index(levels, FM.LOG_LEVEL)) {
            return true
        }
        
        // okay, log this message out to console (if console.log exists)
        if (console && typeof console.log !== 'undefined') {
            console.log(level + ': ' + message)
            return true
        }
    }
})
// methods to support looping within magnet buckets
FM.include('magnets.looping', {
    // perform the cycling of updates in a "cycle loop"
    'cycle': function(updates, limit, anim) {
        var $ = FM.$
        
        // get the updates that are shown
        var updates_shown_now = updates.filter(':visible')

        // figure out what updates should be shown next
        var updates_to_be_shown = updates_shown_now.eq(0).prevAll('.__update__')
        var len = updates_to_be_shown.length
        if (len >= limit) {
            // NOTE: This may look at first glance like a bug, and it should be limit * -1
            //   However, the current code is correct. The reason for this is because .prevAll()
            //   (used above) reverses the index order of the elements being selected.
            updates_to_be_shown = updates_to_be_shown.slice(0, limit)
        }
        else if (len > 0) {
            // I have some updates, but not enough...get the remainder from the end...
            updates_to_be_shown = updates_to_be_shown.add(updates.filter(':hidden').slice((limit - len) * -1))
        }
        else {
            // we were at the beginning of the list...therefore, what I
            // want is the last n updates in the list that are hidden
            updates_to_be_shown = updates.filter(':hidden').slice(limit * -1)
        }

        // okay, I now have the updates shown now and the updates I want to show;
        // I need to iterate over these lists and change them out...
        updates_shown_now.each(function() {
            // I have one update going away, and one update coming in...
            var going = $(this)
            var coming = updates_to_be_shown.first()

            // get the update coming off of the to be shown stack
            updates_to_be_shown = updates_to_be_shown.slice(1)

            // make the changeout
            if (going.length > 0 && anim.type === 'fade') {
                going[anim.hide]('fast', function() {
                    coming[anim.show]('fast')
                })
            }
            else {
                going[anim.hide]('fast')
                coming[anim.show]('fast')
            }
        })
    },
    
    
    // perform the rolling of updates in a "roll loop"
    'roll': function(updates, limit, anim) {
        var $ = FM.$
        
        // get the updates that are shown
        var updates_shown_now = updates.filter(':visible')

        // in the normal case, I want to hide the oldest update
        // in the list and show the next newest update
        var to_hide = updates_shown_now.last()
        var to_show = updates_shown_now.eq(0).prev('.__update__')

        // sanity check: what if there is no update newer than the
        // one of the top? this is a non-trivial problem case, but I will
        // punt it by running cycle(), which will push us back to the end
        if (to_show.length === 0) {
            return FM.magnets.looping.cycle(updates, limit, {
                'type': 'fade',
                'show': 'fadeIn',
                'hide': 'fadeOut'
            })
        }

        // okay, perform the roll
        if (to_hide.length > 0 && anim.type === 'fade') {
            to_hide[anim.hide]('fast', function() {
                to_show[anim.show]('fast')
            })
        }
        else {
            to_hide[anim.hide]('fast')
            to_show[anim.show]('fast')
        }
    },
    
    
    // define a recent function, which will perform the display of updates
    // in a "recent" loop -- a recent loop is not actually a true loop, but it
    // performs similarly enough technically that it's grouped here
    'recent': function(updates, limit, anim) {
        var $ = FM.$
        
        // get the updates that are shown
        var updates_shown_now = updates.filter(':visible')

        // if there are updates showing, I need to get the immediately
        // preceding ones; if there are no updates showing, I just need
        // to get the updates at the end of the list...
        if (updates_shown_now.length > 0) {
            var updates_to_be_shown = updates_shown_now.eq(0).prevAll('.__update__').filter(':not(.__stale__)').slice(0, limit)
        }
        else {
            var updates_to_be_shown = updates.filter(':not(.__stale__)').slice(-limit)
        }

        // hide the updates that are showing now, and remove them
        // from the DOM -- they're not coming back
        // once that is done, show the new ones
        if (updates_shown_now.length > 0 && anim.type === 'fade') {
            updates_shown_now[anim.hide]('normal', function() {
                // mark this update as stale
                $(this).addClass('__stale__')

                // okay, show the new updates now
                updates_to_be_shown[anim.show]('fast')
            })
        }
        else {
            // make the swapout concurrently
            updates_shown_now[anim.hide]('fast')
            updates_to_be_shown[anim.show]('fast')
        }
    },
    
    
    // function to mark stale updates as stale, and get rid of all
    // stale updates that are not visible
    '_deal_with_staleness': function(updates, stale) {
        var $ = FM.$
        
        // sanity check: if stale is 0, then don't do anything
        if (stale <= 0) {
            return null
        }

        // first, iterate over all the updates and mark the appropriate
        // updates as stale
        updates.each(function() {
            var now = Math.round(new Date().getTime() / 1000)
            var then = parseInt($(this).find('.__unix-timestamp__').text())
            if (now - then > stale) {
                $(this).addClass('__stale__')
            }
        })

        // second, I want to remove any updates that are stale from the DOM
        // unless (a) the update is visible or (b) the update is the most recent
        // update available (and therefore removing it would bork up the
        // AJAX calls for new updates)
        var removal = updates.filter('.__stale__')
        if (removal.length === updates.length) {
            removal.slice(1).filter(':hidden').remove()
        }
        else {
            removal.filter(':hidden').remove()
        }
    }
});