jQuery.fn.extend({
    create3DCarousel: function (/*options*/) {
        //config
        var options = {
            vanishingPoint: arguments[0].vanishingPoint,
            speed: arguments[0].speed
        }

        //3DCarousel class
        $3DCarousel = function (jEl) {
            var parent = this;

            //Extra Class
            /* NODE */
            $Node = function (/*node, x, y, z*/) {
                var self = this;
                /*fields*/
                this.node = arguments[0].node; //DOM node
                this.nodeIMG = { //CONST
                    width: self.node.find("img:first").width(),
                    height: self.node.find("img:first").height()
                }
                this.x = arguments[0].x;
                this.y = arguments[0].y; //CONST
                this.z = arguments[0].z;
                this.angle_TO_FRONT = arguments[0].angle_TO_FRONT;
                this.depth = 10000 - Math.round(Math.abs(this.z));
                this.timer;

                /*methods*/
                //calculate the item dimension relate to its position(z-depth)
                this.calculate = function () {
                    /*caculation*/
                    //Positioning
                    var sinX = Math.cos((-90-this.angle_TO_FRONT)*Math.PI/180);
                    var cosX = Math.sin((-90-this.angle_TO_FRONT)*Math.PI/180);
                    this.z = parent.Radius*(1+cosX);
                    this.depth = 10000 - Math.round(Math.abs(this.z));
                    this.scalingRatio = 1 - this.z/parent.vanishingPoint;
                    this.x = parent.Radius*sinX/this.scalingRatio;
                    //Scaling
                    this.surfaceScalingRatio = Math.pow(this.scalingRatio, 2);
                    //LIs
                    this.node.width( Math.round( this.surfaceScalingRatio*this.nodeIMG.width ) );
                    this.node.height( Math.round( this.surfaceScalingRatio*this.nodeIMG.height ) );
                    //IMG
                    this.node.find("img:first").css("width", Math.round( this.surfaceScalingRatio*this.nodeIMG.width ) );
                    this.node.find("img:first").css("height", Math.round( this.surfaceScalingRatio*this.nodeIMG.height ) );
                }
                //output to browser: coords of viewport is top:0, left:0
                this.outputToScreen = function () {
                    var opacity = Math.round(Math.pow(this.depth/10000, 40)*100);
                    this.node.css({
                        left: parent.Radius + this.x - this.node.find("img:first").width()/2 + "px",
                        top: this.y - this.node.find("img:first").height()/2 + "px",
                        zIndex: this.depth
                    });
                    this.node.find("img:first").css({
                        opacity: opacity/100
                    });
                }
                /* 
                /* choose CLOCWISE as the DEFAULT ROTATION in case
                /* rotate LEFT OR RIGHT are both possible
                */
                //CORE function to move NODE : move node to any angle value
                this.moveNodeToAngle = function (angle) {
                    var node = $(this.node);
                    //angle_TO_FRONT + offset*i until = |rangeAngle|
                    //with i = rangeAngle/|rangeAngle|
                    this.timer = setInterval(function () {
                        if ( self.angle_TO_FRONT >= 180 ) {
                            self.angle_TO_FRONT -= 360;
                        }
                        if ( self.angle_TO_FRONT <= -180 ) {
                            self.angle_TO_FRONT += 360;
                        }
                        var rangeAngle = Math.abs(angle - self.angle_TO_FRONT);
                        if ( rangeAngle > 180 ) {
                            rangeAngle = -angle + self.angle_TO_FRONT;
                        }
                        else if ( rangeAngle < 180 ) {
                            rangeAngle = angle - self.angle_TO_FRONT;
                        }
                        else {
                            rangeAngle = Math.abs(angle - self.angle_TO_FRONT)
                        }
                        var i = rangeAngle/Math.abs(rangeAngle);
                        if ( self.angle_TO_FRONT == angle ) {
                            parent.timerControl.collectGarbageTimer();
                            clearInterval(self.timer);
                        }
                        else {
                            if ( Math.abs(angle-self.angle_TO_FRONT) < parent.speed ) {
                                restAngle = Math.abs(angle-self.angle_TO_FRONT);
                                self.angle_TO_FRONT += restAngle*i;
                            }
                            else {
                                self.angle_TO_FRONT += parent.speed*i;
                            }
                            self.calculate();
                            self.outputToScreen();
                        }
                    }, 1);

                    parent.timerControl.groupTimer.push(this.timer);

                }
                //node event
                this.node.find("a:first").bind("click", function (evt) {
                    if ( self.angle_TO_FRONT != 0 && self.angle_TO_FRONT != 360 ) {
                        parent.queue.rotateToFront(self);
                    }
                    return false;
                });
            }
            /* QUEUE */
            $Queue = function (/*$NodeList:Array*/) {
                var self = this;
                /*field*/
                this.queue = typeof(arguments[0]) != "undefined" ? arguments[0] : [];
                this.head = this.queue.length > 0 ? this.queue[0] : null;

                /*methods*/
                //return the number of items in queue
                this.length = function (item/*type:$Node*/) {
                    return this.queue.length;
                }
                //return the first item out of the queue
                this.serve = function () {
                    var returnItem = this.queue[0];
                    this.queue.splice(0,1);
                    return returnItem;
                }
                //append item to the end of a queue, no return
                this.append = function (item/*type:$Node*/) {
                    this.queue.push(item);
                }
                //prepend item to the front of a queue, no return
                this.prepend = function (item/*type:$Node*/) {
                    var swapArr = new Array();
                    swapArr.push(item);
                    for ( var i = 0 ; i < this.queue ; i++ ) {
                        swapArr.push(this.queue[i]);
                    }
                    this.queue = swapArr;
                }
                //refresh angle of node in queue and clear timer control
                this.refresh = function (callback) {
                    if ( this.head != null ) {
                        /*call external function*/
                        this.head.node.find("a:first").unbind("click", doExtraWork);
                        /*end.call external function*/
                    }
                    parent.timerControl.clear();
                    callback();
                }
                //output the queue's node to browser
                this.show = function (item/*type:$Node*/) {
                    for ( var i = 0 ; i < this.queue.length ; i++ ) {
                        this.queue[i].outputToScreen();
                    }
                }
                //rotate queue by angle
                this.rotateToFront = function (item/*type:$Node*/) {
                    if ( parent.timerControl.isClear() && parent.ready ) {
                        this.refresh(function () {
                            self.head = item;
                            var offsetAngle = item.angle_TO_FRONT;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT - offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                }
                //rotate queue CW
                this.rotateCW = function () {
                    if ( parent.timerControl.isClear() && parent.ready ) {
                        this.refresh(function () {
                            var offsetAngle = parent.angleX;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT + offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                if ( angle == 0 || angle == 360) {
                                    self.head = self.queue[i];
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                }
                //rotate queue CCW
                this.rotateCCW = function () {
                    if ( parent.timerControl.isClear() && parent.ready ) {
                        this.refresh(function () {
                            var offsetAngle = -parent.angleX;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT + offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                if ( angle == 0 || angle == 360 ) {
                                    self.head = self.queue[i];
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                }
            }
            /* TIMER CONTROL */
            $TimerControl = function () {
                this.groupTimer = new Array();
                this.garbageTimer = 0;

                /*methods*/
                //Clear TimerControl
                this.clear = function () {
                    for ( var i = 0 ; i < this.groupTimer.length ; i++ ) {
                        clearInterval(this.groupTimer[i]);
                    }
                    this.groupTimer = new Array();
                    this.garbageTimer = 0;
                }
                //Collect finished timer
                this.collectGarbageTimer = function () {
                    this.garbageTimer++;
                    if ( this.isClear() ) {
                        parent.callback();
                    }
                }
                //isClear: all timer have been collected
                this.isClear = function () {
                    return this.groupTimer.length == this.garbageTimer;
                }
            }

            //Extends function
            jQuery.fn.extend({
                traverse: function (i) {
                    var startItem = this.next();
                    var endItem = typeof(i) == "undefined" ? null : i;
                    var regExp;
                    var items = new Array();
                    if (endItem != null) {
                        do {
                            items.push(startItem);
                            startItem = startItem.next();
                            regExp = startItem.attr("class") != ""
                                     ? new RegExp(startItem.attr("class"), "g")
                                     : null
                        } while ( regExp == null || !(regExp).test(endItem.attr("class")) )
                    }
                    else {
                        do {
                            items.push(startItem);
                            startItem = startItem.next()
                        } while ( startItem.html() != null )
                    }
                    return items;
                }
            });

            //MAIN
            this.vanishingPoint = options.vanishingPoint;
            this.speed = options.speed;
            this.listItem = jEl;
            var classes = this.listItem.attr("class").split(" ");
            for ( var i = 0 ; i < classes.length ; i++ ) {
                if ( classes[i].match(/Theme_/) ) {
                    var theme = "Carousel3D_" + classes[i];
                }
                else {
                    var theme = "Carousel3D_Default";
                }
            }
            this.container = $("<div class=\"" + theme + "\"></div>");
            this.listItem.before(this.container);
            this.listItem.appendTo(this.container);
            //Controler
            this.controller =  $(
                               "<div class=\"Carousel3DController\">\n" +
                               "    <a href=\"#\" title=\"Previous\" class=\"Prev\">Previous</a>" +
                               "    <a href=\"#\" title=\"Next\" class=\"Next\">Next</a>" +
                               "</div>"
                               );
            this.controller.appendTo(this.container);
            //Loading Container
            this.loadingContainer = $("<div class=\"LoadingContainer\"><p></p></div>");
            this.loadingContainer.appendTo(this.container);
            this.loadingContainer.css({
                width: this.listItem.outerWidth(),
                height: this.listItem.outerHeight(),
                position: "absolute",
                top: 0,
                left: parent.listItem.get(0).offsetLeft,
                zIndex: 2000
            });
            this.ready = false;
            this.Radius = this.listItem.outerWidth()/2;
            this.Y_BaseLine = this.listItem.outerHeight()/2;
            this.onLoadComplete = function () {
                /*call external function*/
                this.queue.head.node.find("a:first").bind("click", doExtraWork);
                //showBenefit(this.queue.head.node.find("a:first").attr("rel"));;
                setTitle(this.queue.head.node.find("img:first").attr("alt"));
                /*end.call external function*/
            }
            this.callback = function () {
                /*call external function*/
                this.queue.head.node.find("a:first").bind("click", doExtraWork);
                showBenefit(this.queue.head.node.find("a:first").attr("rel"));
                setTitle(this.queue.head.node.find("img:first").attr("alt"));
                /*end.call external function*/
            }

            //Timer
            this.timerControl = new $TimerControl();

            //set up list
            parent.listItem.children("li").css({
                opacity: 0
            });
            //setup items in list
            this.listItem.children("li").each(function () {
                var item = $(this);
                item.css({
                    position: "absolute"
                })
            });

            //set up circle
            var items = this.listItem.children("li");
            var images = new Array();
            this.angleX = 360/items.length; //in DEGREE

            //Distribute item onto the circle, except FRONT_NODE (first item)
            this.queue = new $Queue();
            for ( var i = 0 ; i < items.length ; i++ ) {
                var angle = this.angleX*i <= 180 ? this.angleX*i : this.angleX*i - 360;
                var sinX = +Math.cos((90+angle)*Math.PI/180);
                var cosX = -Math.sin((90+angle)*Math.PI/180);
                var NODE = new $Node({
                    node: $(items[i]),
                    x: this.Radius*sinX,
                    y: this.Y_BaseLine,
                    z: this.Radius*(1+cosX),
                    angle_TO_FRONT: angle
                });

                //caculation
                NODE.calculate();
                //append to queue
                this.queue.append(NODE);
                //events to FRONT_NODE
                if ( i == 0 ) {
                    this.queue.head = NODE;
                }

                //images list
                images.push($(items[i]).find("img:first").attr("src"));
            }
            //output to screen
            new PreloadImages(images, function () {
                parent.loadingContainer.fadeOut("slow", function() {
                    parent.listItem.children("li").animate({
                        opacity: 1
                    });
                });
                parent.ready = true;
                parent.queue.show();
                parent.onLoadComplete();

            } , this.loadingContainer.find("p:first").get(0));

            //events: NEXT and PREV
            this.controller.find(".Next:first").bind("click", function () {
                parent.queue.rotateCW(); //CW = clockwise
                return false;
            });
            this.controller.find(".Prev:first").bind("click", function () {
                parent.queue.rotateCCW(); //CCW = counter-clockwise
                return false;
            });
        }

        //setup
        $(this).each(function () {
            new $3DCarousel($(this));
        })
    }
});

function doExtraWork () { //onclick event on FRONT NODE
    showBenefit($(this).attr("rel"));
}

function setTitle (title/*String*/) {
    $("#cardnameHolder").html(title);
}

function hideBenefit () {
    $("#benefitContent li.Fake").hide();
    if ( typeof(arguments[0]) != "undefined" && arguments[0] ) { //CLOSE
        $("#benefitContent li.Fake").remove();
        $("#benefitContent li.FlyIn").animate({
            left: "-2000px"
        }, "normal", "swing", function () {
            $("#benefitContent li:not(.FlyIn)").css({
                left: "-2000px"
            });
        });
        $("#benefitContent li.FlyIn").removeClass("FlyIn");
    }
    else {
        $("#benefitContent li.FlyIn").animate({
            opacity: 0
        }, "normal", "swing", function () {
            $("#benefitContent li.FlyIn").removeClass("FlyIn");
        });
    }
}
function showBenefit (str/*String*/) {
    var activeBenefitName = str;
    if ( $("li#" + activeBenefitName).hasClass("FlyIn") ) {
        //same programme
        $("#benefitContent li.Fake").css({
            opacity: 100,
            display: "block"
        });
        $("li#" + activeBenefitName).animate({
            opacity: 0
        }, "normal", "swing", function () {
            $("li#" + activeBenefitName).animate({
                opacity: 100
            }, "normal", "swing");
        });
    }
    else {
        if ( $("#benefitContent li.FlyIn").length > 0 ) {
            hideBenefit();
        }
        $("li#" + activeBenefitName).animate({
            left: 0,
            opacity: 100
        }, "normal", "swing", function () {
            $("li#" + activeBenefitName).addClass("FlyIn");
            $("#benefitContent li:not(.FlyIn)").css({
                opacity: 0,
                left: 0
            });
            if ( $("#benefitContent li.Fake").length <= 0 ) {
                $("#benefitContent").append("<li class=\"Fake\"></li>");
                $("#benefitContent li.Fake").css({
                    opacity: 100,
                    display: "none"
                });
            }
        });
    }
}