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'
        scrollSpeed: 500, // pages sliding speed in milliseconds
        adjustHeight: true, // auto-adjust height for each page
        indicateAJAX: true, // show "Loading..." indicator on AJAX calls
        indicateAnimation: true, // show "Loading..." indicator on pages sliding
        skipImageLoading: false, // don't wait until page images will load (needed to calculate exact page height)
        cache: false, // pages may be cached
        enabled: false,

        // Special inner objects. See ListWidgetPaginator and ListWidgetAutoSlider bellow
        paginator: null,
        autoSlider: null,

        // Debug / 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.initImages();
            this.initCache();
            this.initNavigation();
            this.initAutoSliding();
        },

        initImages: function() {
            var self = this;

            function _loaded() {
                self.listContainer.height(self.listContainer.height());
                self.enableLayout();
            }

            if (!this.skipImageLoading) {
                this.loadImages(this.itemsList, true).done(_loaded);
            } else {
                _loaded();
            }
        },

        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;
            }
        },

        initNavigation: function() {
            var self = this;

            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);
            });

            this.initPaginator();
        },

        initPaginator: function() {
            if (this.paginator) {
                var cfg = $.extend(true, {}, this.paginator);
                cfg.listWidget = this;
                this.paginator = new ListWidgetPaginator(cfg);
            }
        },

        initAutoSliding: function() {
            if (this.autoSlider) {
                var cfg = $.extend(true, {}, this.autoSlider);
                cfg.listWidget = this;
                this.autoSlider = new ListWidgetAutoSlider(cfg);
            }
        },

        loadNext: function() {
            if (this.nextPage) {
                this.loadPage(parseInt(this.page, 10) + 1);
            } else if (this.page != 1) {
                this.loadPage(1);
            }
        },

        loadPage: function(num) {
            if (!this.enabled)
                return false;

            this.trigger('pageload', num);

            if (this.cache && this.cache[num]) { // cache
                this.loadPageFromCache(num);
            } else { // AJAX
                this.loadPageFromServer(num);
            }
        },

        loadPageFromCache: function(num) {
            this.updateLayout(this.cache[num]);
        },

        loadPageFromServer: function(num) {
            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);
                    } catch(e) {
                        data = {};
                    }
                })
                .fail(function() {
                    self.enableLayout();
                });
        },

        disableLayout: function(showLoading) {
            this.enabled = false;

            this.prevBtn.addClass(this.btnLoadingClass);
            this.nextBtn.addClass(this.btnLoadingClass);

            if (showLoading)
                this.loadingMask.show();

            this.trigger('disabled');
        },

        enableLayout: function() {
            this.enabled = true;

            this.prevBtn.removeClass(this.btnLoadingClass);
            this.nextBtn.removeClass(this.btnLoadingClass);

            this.loadingMask.hide();

            this.trigger('enabled');
        },

        updateLayout: function(data) {
            if (this.validateData(data)) {
                var self = this;
                var listContainer = this.listContainer;
                var oldItemsList = this.itemsList;
                var oldHeight = oldItemsList.outerHeight(true);
                var isNext = data.page > this.page;

                //----------------------------------- 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;

                this.trigger('pageloaded', 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) }, self.scrollSpeed, scroll.resolve);
                else
                    newPage.css({marginTop: -newPage.outerHeight(true)}).animate({ marginTop: 0 }, self.scrollSpeed, scroll.resolve);
            } else if (this.layout == 'horizontal') {
                if (isNext)
                    oldPage.animate({ marginLeft: -oldPage.outerWidth(true) }, self.scrollSpeed, scroll.resolve);
                else
                    newPage.css({marginLeft: -newPage.outerWidth(true)}).animate({ marginLeft: 0 }, self.scrollSpeed, 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;
        },

        bind: function() {
            return this.container.bind.apply(this.container, arguments);
        },

        trigger: function() {
            return this.container.trigger.apply(this.container, arguments);
        }
    };

    window.ListWidget = ListWidget;


    /**
     * Inner object: paginator
     */
    function ListWidgetPaginator(cfg) {
        if (cfg)
            this.init(cfg);
    }

    ListWidgetPaginator.prototype = {
        listWidget: null,
        container: '.pages-navigation-block',
        pageItems: null,
        pageLinks: null,
        selectedClass: 'selected-page',
        enabled: true,

        init: function(cfg) {
            $.extend(true, this, cfg);

            if (!this.listWidget)
                return;

            this.container = this.listWidget.container.find(this.container);
            this.pageItems = this.container.find('> li');
            this.pageLinks = this.pageItems.find('> a');

            this.initPaging();
            this.initRelations();
        },

        initPaging: function() {
            var self = this;

            this.pageLinks.click(function(e) {
                e.preventDefault();

                if (self.enabled) {
                    var page = $(this).attr('rel').match(/page-(\d+)$/);
                    if (page) {
                        page = page[1];
                        self.listWidget.loadPage(page);
                    }
                }
            });
        },

        initRelations: function() {
            var self = this;

            this.listWidget.bind('disabled', function() {
                self.disableLayout();
            });

            this.listWidget.bind('enabled', function() {
                self.enableLayout();
            });

            this.listWidget.bind('pageloaded', $.proxy(this, 'updateLayout'));
        },

        disableLayout: function() {
            this.enabled = false;
        },

        enableLayout: function() {
            this.enabled = true;
        },

        updateLayout: function(e, data) {
            this.pageItems.removeClass(this.selectedClass);
            this.pageLinks.filter('[rel="page-' + data.page + '"]').parent().addClass(this.selectedClass);
        }
    };

    /**
     * Inner object: auto-slider
     */
    function ListWidgetAutoSlider(cfg) {
        if (cfg)
            this.init(cfg);
    }

    ListWidgetAutoSlider.prototype = {
        timeout: 7000, // delay in milliseconds
        listWidget: null,
        notesArea: '.auto-slider-notes',
        _timer: null,  // setTimeout() ID
        _blockers: null, // what is blocking auto-scrolling now

        _: {
            MOUSEENTER_TO_STOP: "Работает слайд-шоу. Наведите мышкой на слайд, чтобы приостановить его",
            MOUSELEAVE_TO_START: "Слайд-шоу на паузе. Уведите мышку с области слайда, чтобы возобновить его"
        },

        init: function(cfg) {
            $.extend(true, this, cfg);

            if (!this.listWidget)
                return;

            this.notesArea = this.listWidget.container.find(this.notesArea);

            this._blockers = {};
            var self = this;

            this.listWidget.bind('disabled', function() {
                self._blockers.widgetDisabled = true;
                self.stopSliding();
            });

            this.listWidget.bind('mouseenter', function() {
                self._blockers.mouseEntered = true;
                self.notesArea.html(self._.MOUSELEAVE_TO_START);
                self.stopSliding();
            });

            this.listWidget.bind('enabled', function() {
                delete self._blockers.widgetDisabled;
                self.startSliding();
            });

            this.listWidget.bind('mouseleave', function() {
                delete self._blockers.mouseEntered;
                self.notesArea.html(self._.MOUSEENTER_TO_STOP);
                self.startSliding();
            });

            self.notesArea.html(self._.MOUSEENTER_TO_STOP);

            this.startSliding();
        },

        isEnabled: function() {
            for (var k in this._blockers)
                if (this._blockers.hasOwnProperty(k))
                    return false;
            return true;
        },

        startSliding: function() {
            if (!this.isEnabled())
                return false;

            var self = this;

            if (this.timeout) {
                this._timer = setTimeout(function() {
                    self.listWidget.loadNext();
                    self.startSliding();
                }, this.timeout);
            }
        },

        stopSliding: function() {
            clearTimeout(this._timer);
            this._timer = null;
        }
    };
})(jQuery);
