
(function($) {
    // Reflection object.
    function Reflection(img, reflHeight, opacity) {
        var reflection, cntx, imageWidth = img.width, imageHeight = img.width, gradient, parent;

        parent = $(img.parentNode);
        if ($.browser.msie) {
            var added = parent.append("<div class='reflection' style='background-color: Black; position:absolute;' ><img src='" + img.src + "' class='reflimg' style='width: 100%; height: 100%;'/></div>");
            this.element = added.find('.reflection')[0];
            reflection = added.find('.reflimg')[0];
            reflection.style.filter = "flipv progid:DXImageTransform.Microsoft.Alpha(opacity=" + (opacity * 100) + ", style=1, finishOpacity=0, startx=0, starty=0, finishx=0, finishy=" + (reflHeight / imageHeight * 100) + ")";
        }
        else {
            this.element = reflection = parent.append("<canvas class='reflection' style='position:absolute'/>").find(':last')[0];
            if (!reflection.getContext) {
                return;
            }
            cntx = reflection.getContext("2d");
            try {
                $(reflection).attr({ width: imageWidth, height: reflHeight });
                cntx.save();
                cntx.translate(0, imageHeight - 1);
                cntx.scale(1, -1);
                cntx.drawImage(img, 0, 0, imageWidth, imageHeight);
                cntx.restore();
                cntx.globalCompositeOperation = "source-over";
                gradient = cntx.createLinearGradient(0, 0, 0, reflHeight);
                gradient.addColorStop(0, "rgba(0, 0, 0, " + (1 - opacity) + ")");
                gradient.addColorStop(1, "rgba(0, 0, 0, 1.0)");
                cntx.fillStyle = gradient;
                cntx.fillRect(0, 0, imageWidth, reflHeight);
            } catch (e) {
                return;
            }
        }
        // Store a copy of the alt and title attrs into the reflection
        $(reflection).attr({ 'alt': $(img).attr('alt'), title: $(img).attr('title') });

    } // Reflection object

    // Item object.
    var Item = function(imgIn, options) {
        this.orgWidth = imgIn.width;
        this.orgHeight = imgIn.height;
        this.image = imgIn;
        this.reflection = null;
        this.alt = imgIn.alt;
        this.title = imgIn.title;
        this.imageOK = false;
        this.options = options;

        this.imageOK = true;

        if (this.options.reflHeight > 0) {
            this.reflection = new Reflection(this.image, this.options.reflHeight, this.options.reflOpacity);
        }
        $(this.image).css('position', 'absolute');
    }; // END Item object

    // Controller object.
    var Controller = function(container, images, options) {
        var items = [], funcSin = Math.sin, funcCos = Math.cos, ctx = this;
        this.controlTimer = 0;
        this.stopped = false;
        //this.imagesLoaded = 0;
        this.container = container;
        this.xRadius = options.xRadius;
        this.yRadius = options.yRadius;
        this.showFrontTextTimer = 0;
        this.autoRotateTimer = 0;
        if (options.xRadius === 0) {
            this.xRadius = ($(container).width() / 2); // PFS
        }
        if (options.yRadius === 0) {
            this.yRadius = ($(container).height() / 2);
        }

        this.xCentre = options.xPos;
        this.yCentre = options.yPos;
        this.frontIndex = 0; // Index of the item at the front
        this.updateCount = 0;

        //this.rotation = Math.PI;
        //this.destRotation = 0;
        this.timeDelay = 1000 / options.FPS;
        this.itemSpacing = options.itemSpacing;
        this.tick = null;

        this.nextContainer = 0;

        // Turn on the infoBox
        if (options.altBox !== null) {
            $(options.altBox).css('display', 'block');
            $(options.titleBox).css('display', 'block');
        }
        // Turn on relative position for container to allow absolutely positioned elements
        // within it to work.
        /*$(container).css({ position: 'absolute', overflow: 'hidden' });*/

        $(options.buttonLeft).css('display', 'inline');
        $(options.buttonRight).css('display', 'inline');

        // Setup the buttons.
        $(options.buttonLeft).bind('mouseup', this, function(event) {
            event.data.rotate(-1);
            return false;
        });
        $(options.buttonRight).bind('mouseup', this, function(event) {
            event.data.rotate(1);
            return false;
        });

        // You will need this plugin for the mousewheel to work: http://plugins.jquery.com/project/mousewheel
        if (options.mouseWheel) {
            $(container).bind('mousewheel', this, function(event, delta) {
                event.data.rotate(delta);
                return false;
            });

            $.extend($.support, {
                touch: typeof Touch == "object"
            });

            var lastX, lastY;
            function touchStart(event) {
                if (event.touches.length != 1)
                    return;
                var touch = event.touches[0];
                lastX = touch.pageX;
                lastY = touch.pageY;
            }

            function touchMoved(event) {
                if (event.touches.length != 1)
                    return true;

                var touch = event.changedTouches[0];
                event.preventDefault();
                var x = touch.pageX;
                if (lastX > 0) {
                    $("#carousel1").data('carousel').rotate((x - lastX) / 200);
                }
                lastX = x;
            }

            if ($.support.touch) {
                var ths = document.getElementById("carousel1");
                ths.addEventListener('touchstart', touchStart, false);
                ths.addEventListener('touchmove', touchMoved, false);
            }
        }

        $(container).bind('mouseover click', this, function(event) {
            clearInterval(event.data.autoRotateTimer); 	// Stop auto rotation if mouse over.
            var text = $(event.target).attr('alt');
            // If we have moved over a carousel item, then show the alt and title text.

            if (text !== undefined && text !== null) {

                clearTimeout(event.data.showFrontTextTimer);
                $(options.altBox).html(($(event.target).attr('alt')));
                $(options.titleBox).html(($(event.target).attr('title')));
                $(options.altBox).stop().animate({ opacity: 1.0 }, 500);
                $(options.titleBox).stop().animate({ opacity: 1.0 }, 500);

                if (options.bringToFront && event.type == 'click') {
                    var idx = $(event.target).data('itemIndex');
                    var frontIndex = event.data.frontIndex;
                    var diff = idx - frontIndex;
                    event.data.rotate(-diff);
                }
            }
        });
        // If we have moved out of a carousel item (or the container itself),
        // restore the text of the front item in 1 second.
        $(container).bind('mouseout', this, function(event) {
            var context = event.data;
            clearTimeout(context.showFrontTextTimer);
            //context.showFrontTextTimer = setTimeout(function() { context.showFrontText(); }, 1000);
            context.hideFrontText();
            context.autoRotate(); // Start auto rotation.
        });

        // Prevent items from being selected as mouse is moved and clicked in the container.
        $(container).bind('mousedown', this, function(event) {
            event.data.container.focus();
            return false;
        });
        container.onselectstart = function() {
            return false; // For IE.
        };

        this.innerWrapper = $(container).wrapInner('<div style="position:absolute; width:100%; height:100%;"/>').children()[0];

        this.showFrontText = function() {
            if (items[this.frontIndex] === undefined)
                return; 	// Images might not have loaded yet.
            $(options.titleBox).html($(items[this.frontIndex].image).attr('title'));
            $(options.altBox).html($(items[this.frontIndex].image).attr('alt'));
            $(options.altBox).stop().animate({ opacity: 1.0 }, 500);
            $(options.titleBox).stop().animate({ opacity: 1.0 }, 500);
        };

        this.hideFrontText = function() {
            $(options.altBox).stop().animate({ opacity: 0.0 }, 500);
            $(options.titleBox).stop().animate({ opacity: 0.0 }, 500);
        };

        this.reset = function() {
            this.stop();
            this.frontIndex = 0;
            this.rotation = items.length * this.itemSpacing;
            this.destRotation = Math.PI / 2; // -((items.length / 2) >> 0) * this.itemSpacing;
            this.updateAll();
            this.go();
        }

        this.rollout = function() {
            clearInterval(this.autoRotateTimer);
            this.destRotation = this.rotation = -10 * Math.PI;
        }

        this.reload = function(container, spacing) {
            this.nextContainer = container;

            this.destRotation = -3 * Math.PI;
            this.updateCount = 0;
            this.go();
        }

        this.loadNext = function() {
            for (var i = 0; i < items.length; i++) {
                var item = items[i];
                if (item.imageOK) {
                    var img = item.image;
                    img.width = item.orgWidth;
                    img.height = item.orgHeight;
                }
            }
            items.length = 0;
            images.length = 0;
            $('.reflection', this.container).remove();
            images = $('.carouselitem', this.nextContainer);
            this.tt = setInterval(function() { ctx.checkImagesLoaded(); }, 50);
            this.nextContainer = 0;
            this.updateCount = 0;
        };

        this.go = function() {
            var now = new Date();
            this.tick = now.getTime();
            if (this.controlTimer !== 0)
                return;
            var context = this;
            this.controlTimer = setTimeout(function() { context.updateAll(); }, this.timeDelay);
        };

        this.stop = function() {
            clearTimeout(this.controlTimer);
            this.controlTimer = 0;
        };

        // Starts the rotation of the carousel. Direction is the number (+-) of carousel items to rotate by.
        this.rotate = function(direction) {
            this.frontIndex -= direction;
            this.frontIndex %= items.length;
            this.destRotation += this.itemSpacing * direction;

            if (this.destRotation > Math.PI / 2)
                this.destRotation = Math.PI / 2;
            else if (this.destRotation < Math.PI / 2 - (items.length - 1) * this.itemSpacing)
                this.destRotation = Math.PI / 2 - (items.length - 1) * this.itemSpacing;

            this.hideFrontText();
            this.go();
        };

        this.autoRotate = function() {
            clearInterval(this.autoRotateTimer);
            if (options.autoRotate !== 'no') {
                var dir = (options.autoRotate === 'right') ? 1 : -1;
                this.autoRotateTimer = setInterval(function() {
                    if (ctx.rotation <= -Math.PI / 2 + Math.PI / 7)
                        ctx.rotate(100);
                    else
                        ctx.rotate(dir);
                }, options.autoRotateDelay);
            }
        };

        // This is the main loop function that moves everything.
        this.updateAll = function() {
            var minScale = options.minScale; // This is the smallest scale applied to the furthest item.
            var smallRange = 0.5 / (1 + minScale); // PFS!
            var w, h, x, y, scale, item, sinVal;

            var change = (this.destRotation - this.rotation);

            if (change < -0.6)
                change = -0.6;
            else if (change > 0.6)
                change = 0.6;

            var absChange = Math.abs(change);

            if (this.nextContainer) {
                ++this.updateCount;
                change = -1.0;
            }

            var now = new Date();
            var tick = now.getTime();
            var diff = (tick - this.tick);
            if (diff > 200 || diff < 30)
                diff = this.timeDelay;
            this.rotation += change * diff / this.timeDelay * options.speed;
            this.tick = tick;

            if (absChange < 0.001) {
                this.rotation = this.destRotation;
            }

            var itemCount = items.length;

            if (this.itemSpacing == 0)
                this.itemSpacing = Math.PI / itemCount;

            var rad = this.rotation;
            var isMSIE = $.browser.msie;

            this.innerWrapper.style.display = 'none';

            var style;
            var px = 'px', reflHeight, hidden = 0;
            var context = this;
            for (var i = 0; i < itemCount; i++) {
                item = items[i];

                sinVal = funcSin(rad);
                scale = ((sinVal + 1) * smallRange) + minScale;

                y = this.yCentre + sinVal * this.yRadius;

                scale /= 1.4;
                if (rad > Math.PI / 2 - 0.4 && rad < Math.PI / 2 + 0.4) {
                    factor = 1.4 - Math.abs(Math.PI / 2 - rad);
                    scale *= factor;
                    y /= 1 + (1 - factor) / 3;
                }

                if (rad >= 0 && rad <= Math.PI)
                    x = this.xCentre + (-funcCos(rad) * this.xRadius * scale);
                else {
                    x = -1000;
                }

                if (this.updateCount) {
                    scale /= 1 + this.updateCount / 5 + this.updateCount * this.updateCount / 20;
                    if (scale < 0.05)
                        x = -1000;
                }

                if (x == -1000)
                    ++hidden;

                if (item.imageOK) {
                    var img = item.image;
                    w = img.width = item.orgWidth * scale;
                    h = img.height = item.orgHeight * scale;
                    img.style.left = x - w / 2 + px;
                    img.style.top = y - h / 2 + px;
                    img.style.zIndex = "" + (scale * 50) >> 0;
                    if (item.reflection !== null) {
                        reflHeight = options.reflHeight * scale;
                        style = item.reflection.element.style;
                        if (sinVal < 0.0)
                            x = -1000;
                        style.left = x - w / 2 + px;
                        style.top = y + h / 2 + options.reflGap + px;
                        style.width = w + px;
                        style.height = h + px;
                        style.zIndex = "" + (scale * 50 - 1) >> 0;
                        if (isMSIE)
                            style.filter.finishy = (reflHeight / h * 100);
                        else
                            style.height = reflHeight + px;
                    }
                }
                rad += this.itemSpacing;
            }
            // Turn display back on.
            this.innerWrapper.style.display = 'block';

            // If we have a preceptable change in rotation then loop again next frame.
            if (absChange >= 0.01)
                this.controlTimer = setTimeout(function() { context.updateAll(); }, this.timeDelay);
            else {
                this.stop(); // Otherwise just stop completely.
            }

            if (this.nextContainer && (this.rotation <= this.destRotation || hidden == itemCount)) {
                this.stop();
                this.loadNext();
            }

        }; // END updateAll

        // Check if images have loaded. We need valid widths and heights for the reflections.
        this.checkImagesLoaded = function() {
            var i;
            for (i = 0; i < images.length; i++) {
                if ((images[i].width === undefined) || ((images[i].complete !== undefined) && (!images[i].complete))) {
                    return;
                }
                images[i].top = '0px';
            }
            for (i = 0; i < images.length; i++) {
                items.push(new Item(images[i], options));
                $(images[i]).data('itemIndex', i);
            }
            // If all images have valid widths and heights, we can stop checking.
            clearInterval(this.tt);
            //this.showFrontText();
            this.autoRotate();
            this.reset();
        };

        this.tt = setInterval(function() { ctx.checkImagesLoaded(); }, 50);
    }; // Controller object

    // The jQuery plugin part. Iterates through items specified in selector and inits a Controller class for each one.
    $.fn.Carousel = function(images, options) {
        this.each(function() {
            options = $.extend({}, {
                reflHeight: 0,
                reflOpacity: 0.3,
                reflGap: 6,
                minScale: 0.5,
                xPos: 0,
                yPos: 0,
                xRadius: 0,
                yRadius: 0,
                altBox: null,
                titleBox: null,
                FPS: 30,
                autoRotate: 'no',
                autoRotateDelay: 10000,
                speed: 0.1,
                mouseWheel: false,
                bringToFront: false,
                itemSpacing: 0
            }, options);
            // Create a Controller for each carousel
            $(this).data('carousel', new Controller(this, images, options));
        });
        return this;
    };

})(jQuery);
