if (typeof ListWidget == "undefined") (function($) {
    //====================================================== Tools =====================================================
    /**
     * Image loading tool.
     * .load() not always fires if IMG was cached.
     *
     * Based on Paul Irish "imagesLoaded" jQuery plugin - http://gist.github.com/268257
     * Modified by Kottenator
     */
    $.fn.loadImages = function(callback, context) {
        var elems = this.filter('img'),
            len = elems.length,
            blank = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";

        callback = callback || $.noop;
        context = context || elems;

        function countdown() {
            if (this.src != blank) {
                if (--len <= 0)
                    callback.call(context, this);
            }
        }

        elems.each(function() {
            var src = this.src;
            this.src = blank;
            $(this).one('load error', countdown);
            this.src = src;
        });

        if (!elems.length)
            callback.call(context, this);

        return this;
    };

    //=================================================== Widget code ==================================================
    function ListWidget(cfg) {
        if (cfg)
            this.init(cfg);
    }

    ListWidget.prototype = {
        container: null,
        listContainer: '.list-container',
        itemsList: '.items-list',
        prevBtn: '.prev-btn',
        nextBtn: '.next-btn',
        loadingMask: '.loading-mask',
        btnLoadingClass: 'btn-loading',
        btnDisabledClass: 'btn-disabled',
        page: 1,
        prevPage: null,
        nextPage: null,
        url: '',
        params: null,
        layout: 'vertical', // or 'horizontal'
        adjustHeight: true,
        indicateAJAX: true,
        indicateAnimation: true,
        skipImageLoading: false,
        cache: false,
        // Tests
        console: window.console || { log: $.noop },
        DEBUG: false,

        init: function(cfg) {
            var self = this;

            cfg = cfg || {};
            if (typeof cfg == "string")
                cfg = { container: cfg };
            $.extend(true, this, cfg);

            this.container = $(this.container);
            this.listContainer = this.container.find(this.listContainer);
            this.itemsList = this.container.find(this.itemsList);
            this.prevBtn = this.container.find(this.prevBtn);
            this.nextBtn = this.container.find(this.nextBtn);
            this.loadingMask = this.container.find(this.loadingMask);
            this.loadImages(this.itemsList, true)
                .done(function() {
                    self.listContainer.height(self.listContainer.height());
                    self.enableLayout();
                });

            this.initCache();

            this.prevBtn.click(function(e) {
                e.preventDefault();
                var btn = $(this);
                if (btn.hasClass(self.btnLoadingClass) || btn.hasClass(self.btnDisabledClass))
                    return;
                if (self.prevPage)
                    self.loadPage(self.prevPage);
            });

            this.nextBtn.click(function(e) {
                e.preventDefault();
                var btn = $(this);
                if (btn.hasClass(self.btnLoadingClass) || btn.hasClass(self.btnDisabledClass))
                    return;
                if (self.nextPage)
                    self.loadPage(self.nextPage, true);
            });
        },

        initCache: function() {
            if (this.cache) {
                this.cache = {};
                var data = {
                    page_html: this.itemsList.html(),
                    page: this.page,
                    prev_page: this.prevPage,
                    next_page: this.nextPage
                };
                this.cache[this.page] = data;
            }
        },

        loadPage: function(num, is_next) {
            if (this.cache && this.cache[num]) { // cache
                this.loadPageFromCache(num, is_next);
            } else { // AJAX
                this.loadPageFromServer(num, is_next);
            }
        },

        loadPageFromCache: function(num, is_next) {
            this.updateLayout(this.cache[num], is_next);
        },

        loadPageFromServer: function(num, is_next) {
            var self = this;
            this.disableLayout(this.indicateAJAX);
            $.get(this.url, $.extend(this.params, { page: num }))
                .done(function(data) {
                    self.enableLayout();
                    try {
                        data = $.parseJSON(data);
                        self.updateLayout(data, is_next);
                    } catch(e) {
                        data = {};
                    }
                })
                .fail(function() {
                    self.enableLayout();
                });
        },

        disableLayout: function(showLoading) {
            this.prevBtn.addClass(this.btnLoadingClass);
            this.nextBtn.addClass(this.btnLoadingClass);
            if (showLoading)
                this.loadingMask.show();
        },

        enableLayout: function() {
            this.prevBtn.removeClass(this.btnLoadingClass);
            this.nextBtn.removeClass(this.btnLoadingClass);
            this.loadingMask.hide();
        },

        updateLayout: function(data, isNext) {
            if (this.validateData(data)) {
                var self = this;
                var listContainer = this.listContainer;
                var oldItemsList = this.itemsList;
                var oldHeight = oldItemsList.outerHeight(true);

                //----------------------------------- Step 1: prepare new page ---------------------------------------------
                var newItemsList = oldItemsList.clone().empty().css({ visibility: 'hidden', position: 'absolute' });
                if (isNext)
                    newItemsList.insertAfter(oldItemsList);
                else
                    newItemsList.insertBefore(oldItemsList);
                $(data.page_html).appendTo(newItemsList);
                //var newHeight = newItemsList.outerHeight(true); // it's too early to calculate total height

                //----------------------------------- Step 2: prepare loading parts ----------------------------------------
                // Image loading defer
                var loadImages = function() { return self.loadImages(newItemsList, true); };
                // Animate scroll defer
                var animateScroll = function() { return self.animateScroll(oldItemsList, newItemsList, isNext, true); };
                // Animate container resize defer
                var animateResize = function() { return self.animateContainerResize(listContainer, newItemsList.outerHeight(true), true); };
                // Enable layout after all - isn't Deferred
                var enableLayout = $.proxy(self,'enableLayout');

                //----------------------------------- Step 3: Let's chain it! ----------------------------------------------
                var animateAll = function() {
                    var newHeight = newItemsList.outerHeight(true); // and now it's time to calculate total height
                    var adjust = self.adjustHeight;

                    if (adjust && oldHeight < newHeight)
                        animateScroll().pipe(animateResize).done(enableLayout);
                    else if (adjust && oldHeight > newHeight)
                        animateResize().pipe(animateScroll).done(enableLayout);
                    else
                        animateScroll().done(enableLayout);
                };

                if (this.skipImageLoading)
                    animateAll();
                else
                    loadImages().done(animateAll);

                //----------------------------------- Step 4: Update data and navigation -----------------------------------
                this.page = data.page;

                var prevPage = this.prevPage = data.prev_page;
                this.prevBtn[prevPage ? 'removeClass' : 'addClass'](this.btnDisabledClass);

                var nextPage = this.nextPage = data.next_page;
                this.nextBtn[nextPage ? 'removeClass' : 'addClass'](this.btnDisabledClass);

                if (this.cache)
                    this.cache[data.page] = data;
            }
        },

        loadImages: function(page, disable) {
            var self = this;

            var imagesLoaded = $.Deferred();
            page.find('img').loadImages(imagesLoaded.resolve);

            if (this.DEBUG)
                imagesLoaded.done(function() { self.console.log('All images loaded'); });

            if (disable)
                this.disableLayout(this.indicateAnimation);

            return imagesLoaded;
        },

        animateScroll: function(oldPage, newPage, isNext, disable) {
            var self = this;

            var scroll = $.Deferred();
            scroll.done(function() {
                oldPage.remove();
                self.itemsList = newPage;
            });

            newPage.css({ visibility: 'visible', position: 'relative' });

            if (this.layout == 'vertical') {
                if (isNext)
                    oldPage.animate({ marginTop: -oldPage.outerHeight(true) }, null, scroll.resolve);
                else
                    newPage.css('margin-top', -newPage.outerHeight(true)).animate({ marginTop: 0 }, null, scroll.resolve);
            } else if (this.layout == 'horizontal') {
                if (isNext)
                    oldPage.animate({ marginLeft: -oldPage.outerWidth(true) }, null, scroll.resolve);
                else
                    newPage.css('margin-left', -newPage.outerWidth(true)).animate({ marginLeft: 0 }, null, scroll.resolve);
            } else {
                scroll.resolve();
            }

            if (this.DEBUG)
                scroll.done(function() { self.console.log('Page scrolled!'); });

            if (disable)
                this.disableLayout(this.indicateAnimation);

            return scroll;
        },

        animateContainerResize: function(listContainer, newHeight, disable) {
            var self = this;
            var resize = $.Deferred();

            listContainer.animate({ height: newHeight }, null, resize.resolve);

            if (disable)
                this.disableLayout(this.indicateAnimation);

            if (this.DEBUG)
                resize.done(function() { self.console.log('Container resized!'); });

            return resize;
        },

        validateData: function(data) {
            var valid = true;
            if (typeof data.page_html == "undefined" || data.page_html == null)
                valid = false;
            if (typeof data.page == "undefined" && data.page == null)
                valid = false;
            return valid;
        }
    };

    window.ListWidget = ListWidget;

})(jQuery);
