﻿"use strict";

window.EnkronaAuctions = window.EnkronaAuctions || (function () {
    var conversionOffset = 621355968000000000, ticksPerMillisecond = 10000, dstMsOffset,
        superklippMainDiv = 'SuperklippMainDiv', // div to populate with new superklipps
        activeSuperklippUrl = '/Auktioner/ActiveSuperklippAuctions.aspx'; // url to fetch superklipps from

    return {
        NetTicksToDate: function (ticks) {
            return new Date((ticks - conversionOffset) / ticksPerMillisecond + new Date().getTimezoneOffset() * 60000);
        },

        DateToNetTicks: function (date) {
            return (date.getTime() - date.getTimezoneOffset() * 60000) * ticksPerMillisecond + conversionOffset;
        },

        // wrappers for effect and Ajax calls to avoid having unassigned new statements (keeps jsLint happy)
        Updater: function (targetId, url, options) {
            return new Ajax.Updater(targetId, url, options);
        },

        BlindUp: function (targetId, options) {
            return new Effect.BlindUp(targetId, options);
        },

        BlindDown: function (targetId, options) {
            return new Effect.BlindDown(targetId, options);
        },

        Pulsate: function (targetId, options) {
            var ieOptions;
            if (Prototype.Browser.IE) {
                ieOptions = {
                    afterFinish: function (e) {
                        e.element.setStyle({ backgroundColor: 'transparent' });
                    },
                    beforeStart: function (e) {
                        e.element.setStyle({ backgroundColor: 'transparent' });
                    }
                };
                return new Effect.Pulsate(targetId, Object.extend(options, ieOptions));
            }
            return new Effect.Pulsate(targetId, options);
        },

        Fade: function (targetId, options) {
            return new Effect.Fade(targetId, options);
        },

        Morph: function (target, options) {
            return new Effect.Morph(target, options);
        },

        UpdateValueOrText: function (targetObj, text) {
            if (typeof targetObj.value !== 'undefined') {
                targetObj.value = text;
            }
            else {
                targetObj.innerHTML = text;
            }
        },

        DSTMillisecondsOffset: function () { // extra hours for daylight savings time
            if (typeof dstMsOffset === 'undefined') {
                var gmt, lsm, lso, day, now;
                now = new Date();
                gmt = new Date(now.getTime());
                lsm = new Date(now.getTime());
                lso = new Date(now.getTime());

                lsm.setMonth(2);
                lsm.setDate(31);
                day = lsm.getDay();
                lsm.setDate(31 - day);
                lsm.setHours(3);
                lsm.setMinutes(0);
                lso.setMonth(9);
                lso.setDate(31);
                day = lso.getDay();
                lso.setDate(31 - day);
                lso.setHours(3);
                lso.setMinutes(0);
                dstMsOffset = ((gmt >= lsm && gmt <= lso) ? 0 : 1) * 3600 * 1000;
            }
            return dstMsOffset;
        },

        AuctionItem: Class.create({
            initialize: function (auctionId, currentBid, nextBid, bidLeader, endDateTicks, seriesId) {
                // log('creating Auction with id: ' + auctionId);
                this.auctionId = auctionId;
                this.update(currentBid, nextBid, bidLeader, endDateTicks);
                this.seriesId = seriesId || '';

                this.bidObj = null;
                this.clockObj = null;
                this.firstUpdate = true;
                this.hasActiveClock = true;
                this.fullFormat = false;
            },

            //toString: function () {
            //    return '\nAuction ID:    ' + this.auctionId + '\n' +
            //        'currentBid:    ' + this.currentBid + '\n' +
            //        'nextBid:    ' + this.nextBid + '\n' +
            //        'bidLeader:    ' + this.bidLeader + '\n' +
            //        'endDateTicks:    ' + this.endDateTicks + '\n' +
            //        'endDate:    ' + this.endDate + '\n\n' +
            //        'series:    ' + this.seriesId + '\n\n' +
            //        '\nloaded bidObj:' + (this.bidObj === null ? 'no' : 'yes') +
            //        '\nloaded clockObj:' + (this.clockObj === null ? 'no' : 'yes') +
            //        '\nHas active clock: ' + this.hasActiveClock + '\n';
            //},

            update: function (currentBid, nextBid, bidLeader, endDateTicks) {
                this.currentBid = currentBid;
                this.nextBid = nextBid;
                this.bidLeader = bidLeader;
                this.endDateTicks = endDateTicks;
                this.endDate = EnkronaAuctions.NetTicksToDate(endDateTicks);
            },

            getBidObj: function () {
                if (this.bidObj === null) {
                    this.bidObj = $('b' + this.auctionId);
                }
                return this.bidObj;
            },

            getClockObj: function () {
                if (this.clockObj === null) {
                    this.clockObj = $('e' + this.auctionId);
                }
                return this.clockObj;
            },

            isSuperklipp: function () {
                return this.seriesId !== '';
            }
        }),

        AuctionUpdater: Class.create({
            initialize: function (interval, bidderId, serverTicks) { // , clientOffset) {
                this.bidderId = bidderId;
                this.lastUpdate = serverTicks; // ticks
                this.updateClientOffsetFromServerTimestamp(serverTicks);
                this.interval = interval; // getAuctionUpdates interval

                // log('server time: ' + serverTicks + ', client offset (ms)' + this.clientOffset);
                this.auctionItems = $H();
                this.series = $H();
                this.newAuctionsOnServer = $H(); // keep track of how many auctions we are NOT showing
                this.pendingSuperklippSeries = $H();
                this.reloadOnPendingSuperklipps = true; // should the page reload when there are no superklipps on the page, but some are pending on the server?

                this.updateExecuter = null;
            },

            updateClientOffsetFromServerTimestamp: function (serverTicks) {
                var tomorrow, theDayAfter;

                this.clientOffset = (new Date()).getTime() - EnkronaAuctions.NetTicksToDate(serverTicks).getTime(); // ms
                tomorrow = this.serverDate();
                tomorrow.setDate(tomorrow.getDate() + 1);
                tomorrow.setHours(0, 0, 0, 0);
                theDayAfter = new Date(tomorrow);
                theDayAfter.setDate(tomorrow.getDate() + 1);
                this.tomorrow = tomorrow;
                this.theDayAfter = theDayAfter;
            },

            //toString: function () {
            //    return '# auction items: ' + this.auctionItems.size() + '\n' +
            //        'Client Offset Time: ' + this.clientOffset + '\n' +
            //        'tomorrow is : ' + this.tomorrow + '\n' +
            //        'the day after is: ' + this.theDayAfter + '\n' +
            //        'last update ticks: ' + this.lastUpdate + ' (' + EnkronaAuctions.NetTicksToDate(this.lastUpdate) + ')\n';
            //},

            add: function (auctionId, currentBid, nextBid, bidLeader, endDateTicks, seriesId) {

                if (auctionId === null) {
                    return;
                }

                var auctionItem = new EnkronaAuctions.AuctionItem(auctionId, currentBid, nextBid, bidLeader, endDateTicks, seriesId);
                this.auctionItems.set(auctionId, auctionItem);
                if (seriesId && typeof seriesId !== 'undefined') {
                    // log('grouping auction ' + auctionId + ' as ' + seriesId);
                    if (typeof this.series.get(seriesId) === 'undefined') {
                        // log('creating ' + seriesId);
                        this.series.set(seriesId, []);
                    }
                    else {
                        auctionItem.hasActiveClock = false; // wait until visible to update the clock
                        this.series.get(seriesId).push(auctionItem);
                    }
                }
                // log('Adding auction object[' + auctionId + ']');
            },

            addCustomAuctionItem: function (auctionItem) {
                this.auctionItems.set(auctionItem.auctionId, auctionItem);
                // log('Adding custom auction object[' + auctionItem.auctionId + ']');
            },


            // flush out all super klipp auctions from the updater, making room for
            // new ones
            flushSuperklipps: function () {
                // console.log('flushing superklipps');
                var self = this;
                this.auctionItems.each(function (key_value) {
                    if (key_value[1].seriesId !== '') {
                        key_value[1].hasActiveClock = false; // prevent further updates
                        self.auctionItems.unset(key_value[0]);
                        // console.log(' > flushing ' + key_value[0] + key_value[1]);
                    }
                });
                this.series = $H();
            },

            refreshSuperklipps: function () {
                if ($(superklippMainDiv) == null) {
                    return null;
                }
                return new Ajax.Updater({ success: superklippMainDiv }, activeSuperklippUrl, { evalScripts: true });
            },

            popNextAuction: function (auctionItem) {
                var series = this.series.get(auctionItem.seriesId),
                    nextAuction = null;

                if (auctionItem.seriesId === '' || typeof series === 'undefined') {
                    return null;
                }

                // making this check before the actual pop so that the series is
                // unset when the last superklipp auction tries to pop the next auction.
                if (series.size() === 0) {
                    this.series.unset(auctionItem.seriesId);
                }

                nextAuction = series.pop();
                return nextAuction || null;
            },

            scheduleClockUpdate: function (auctionItem) {
                var secsToNextUpdate = 0, secondsLeft, msLeft, minsLeft;

                // console.log("scheduling " + auctionItem.auctionId);

                if (auctionItem.hasActiveClock !== true) {
                    return;
                }

                secondsLeft = this.msLeftForAuction(auctionItem) / 1000;
                if (auctionItem.fullFormat === true) {
                    secsToNextUpdate = secondsLeft - Math.floor(secondsLeft);
                }
                else if (secondsLeft < 0) {
                    secsToNextUpdate = 0;
                }
                else if (secondsLeft < 300) {
                    secsToNextUpdate = secondsLeft - Math.floor(secondsLeft);
                }
                else if (secondsLeft < 3600) {
                    // schedule for update on the next minute switch
                    minsLeft = secondsLeft / 60;
                    secsToNextUpdate = (minsLeft - Math.floor(minsLeft)) * 60;
                }
                else { // postpone updates until 1h left
                    secsToNextUpdate = secondsLeft - 3600;
                }

                setTimeout(this.updateClockObject.bindAsEventListener(this, auctionItem), secsToNextUpdate * 1000);
            },

            msLeftForAuction: function (auctionItem) {
                return (auctionItem.endDate.getTime() - this.serverDate().getTime());
            },

            getSecondsToNextClockUpdate: function (auctionItem) {
                var secondsLeft = this.msLeftForAuction(auctionItem) / 1000;

                if (secondsLeft < 300) {
                    return 1;
                }
                else if (secondsLeft < 3600) {
                    return (secondsLeft % 30) + 1;
                }
                else { // postpone updates until 1h left
                    return secondsLeft - 3600;
                }
            },

            serverDate: function () {
                return new Date(new Date() - this.clientOffset);
            },

            getAuctionUpdates: function () {
                var requestServerTime = arguments[1] || false;
                AuctionService.GetAuctionsUpdatedSince(this.lastUpdate, requestServerTime, this.processUpdatedAuctionItems.bindAsEventListener(this));
            },

            processUpdatedAuctionItems: function (result) {
                var updatedAuctionItems = $A(),
                    updatedAuctions = result.A,
                    updatedAuction,
                    auctionItem,
                    i,
                    len;

                updatedAuctions.each(function (updatedAuction) {
                    auctionItem = this.auctionItems.get(updatedAuction.Id) || null;
                    if (auctionItem !== null) {
                        auctionItem.update(updatedAuction.CurrentBid,
                                           updatedAuction.NextBid,
                                           updatedAuction.BidLeader,
                                           updatedAuction.EndDateTicks);
                        updatedAuctionItems.push(auctionItem);
                    }
                    else {
                        // unlisted auction
                        this.newAuctionsOnServer.set(updatedAuction.Id, updatedAuction);
                        if (updatedAuction.SeriesId !== '' && typeof (this.pendingSuperklippSeries.get(updatedAuction.SeriesId)) === 'undefined') {
                            // reload superklipps
                            this.refreshSuperklipps();
                        }
                    }
                }, this);

                if (result.T > 0) {
                    this.lastUpdate = result.T;
                }

                if (result.N > 0) {
                    this.updateClientOffsetFromServerTimestamp(result.N);
                }

                // reload the page when there are pending superklipps on the server and
                // none on the page
                if (this.reloadOnPendingSuperklipps === true && this.series.size() === 0 && this.pendingSuperklippSeries.size() > 0) {
                    // location.href = location.href;
                }
                this.updateBidObjects(updatedAuctionItems);
            },

            updateBidObjects: function (updatedAuctions) {
                var auctionItem, bidObj, i, len, self = this;
                if (updatedAuctions.size() < 1) {
                    return;
                }

                for (i = 0, len = updatedAuctions.length; i < len; i += 1) {
                    auctionItem = updatedAuctions[i];
                    bidObj = auctionItem.getBidObj();
                    if (bidObj !== null) {
                        if (auctionItem.bidLeader === this.bidderId) {
                            if (!bidObj.hasClassName('leadingBid')) {
                                bidObj.addClassName('leadingBid');
                                bidObj.writeAttribute('title', 'Du leder den här auktionen!');
                            }
                        } else {
                            if (bidObj.hasClassName('leadingBid')) {
                                bidObj.removeClassName('leadingBid');
                                bidObj.writeAttribute('title', '');
                            }
                        }

                        (function () {
                            // rebind to new scope
                            var _bidObj = bidObj,
                                _auctionItem = auctionItem,
                                f = function () { self.startBidUpdateAnimation(_bidObj, _auctionItem); };
                            // exec after a while to create an impressionf of a sporadic update
                            setTimeout(f, Math.random() * 1300);
                        })();


                    }
                    else {
                        // tried to update object, but object was missing
                    }
                }
            },

            startBidUpdateAnimation: function (bidObj, auctionItem) {
                var q = { position: 'end', scope: auctionItem.auctionId.toString() };
                // console.log('Updating: ' + bidObj.toString());
                var cur_col = bidObj.getStyle('color');
                bidObj.setStyle({ color: '#888' });
                bidObj.morph('left: -50px;', {
                    duration: 0.2,
                    queue: q,
                    afterFinish: function () {
                        window.EnkronaAuctions.UpdateValueOrText(bidObj, auctionItem.currentBid + ' kr');
                        bidObj.setStyle({ color: cur_col });
                    }
                })
                .morph('left: 0px;', { duration: 0.2, queue: q })
                .morph('left: -20px;', { duration: 0.18, queue: q })
                .morph('left: 0px;', { duration: 0.27, queue: q });
            },

            updateClockObject: function () {
                var auctionItem = arguments[1],
                    endDate = auctionItem.endDate,
                    clockObj = auctionItem.getClockObj(),
                    delta = this.msLeftForAuction(auctionItem),
                    seconds = Math.floor(delta / 1000),
                    remTime = new Date(delta - EnkronaAuctions.DSTMillisecondsOffset()),
                    timeText = '',
                    cD, cH, cM, cS;

                remTime.setTime(remTime.getTime() - ((remTime.getTimezoneOffset() - endDate.getTimezoneOffset()) * 60000));

                if (clockObj === null) {
                    return;
                }

                if (delta <= 0) {
                    timeText = 'Avslutas';
                }
                else if (auctionItem.fullFormat === true) {
                    // todo: daylight savings stuff
                    cD = Math.floor(delta / (24 * 3600 * 1000));
                    cH = remTime.getHours();
                    cM = remTime.getMinutes();
                    cS = remTime.getSeconds();

                    if (delta > 0) {
                        timeText += cD > 0 ? cD + 'd ' : '';
                        timeText += cH > 0 ? cH + 'h ' : '';
                        timeText += cM > 0 ? cM + 'm ' : '';
                        timeText += cS >= 0 ? cS + 's ' : '';
                    }
                }
                else {
                    if (seconds < 60) {
                        timeText = seconds + 's';
                        if (seconds === 59) {
                            clockObj.addClassName('LessThan1');
                        }
                    }
                    else if (seconds < 300) {
                        timeText = remTime.getMinutes() + 'm ' + remTime.getSeconds() + 's';
                        if (seconds === 299) {
                            clockObj.addClassName('LessThan5');
                        }
                    }
                    else if (seconds < 3600) {
                        timeText = remTime.getMinutes() + ' min';
                    }
                    else if (endDate < this.tomorrow) {
                        timeText = String.format("Idag {0:d2}:{1:d2}", endDate.getHours(), endDate.getMinutes());
                    }
                    else if ((endDate > this.tomorrow) && (endDate < this.theDayAfter)) {
                        timeText = String.format("Imorgon {0:d2}:{1:d2}", endDate.getHours(), endDate.getMinutes());
                    }
                    else if (endDate > this.theDayAfter) {
                        timeText = String.format("{0:MMM dd} {1:d2}:{2:d2}", endDate, endDate.getHours(), endDate.getMinutes());
                    }
                }

                // set correct classes on element on first update
                // this is to address the issue with returning from another page
                // and missing the 'switch second'.
                if (auctionItem.firstUpdate === true) {
                    if (seconds < 60 && !clockObj.hasClassName('LessThan1')) {
                        clockObj.addClassName('LessThan1');
                    }
                    else if (seconds < 300 && !clockObj.hasClassName('LessThan5')) {
                        clockObj.addClassName('LessThan5');
                    }
                    auctionItem.firstUpdate = false;
                }

                EnkronaAuctions.UpdateValueOrText(clockObj, timeText);

                if (seconds < -3) { // close the auction 3 seconds after ending it
                    this.closeAuction(auctionItem);
                }
                else {
                    this.scheduleClockUpdate(auctionItem);
                }
            },

            closeAuction: function (auctionItem) {
                var auctionObj = $('r' + auctionItem.auctionId),
                    aid = (auctionObj && auctionObj.id) || '',
                    nextAuctionItem = this.popNextAuction(auctionItem),
                    ancientAuction = this.msLeftForAuction(auctionItem) < -5000,
                    sid = 's' + auctionItem.seriesId;

                // helper to hide old auctions instead of running the provided effect
                // when the auctions are very old. This is to quickly update the
                // page when the user uses the back button to navigate to the page.
                function applyEffect(id, fx) {
                    function hd() {
                        $(id).hide();
                    }
                    (ancientAuction ? hd : fx)();
                }

                if (auctionObj === null) {
                    return;
                }

                if (nextAuctionItem === null) {
                    // the series are contained in a s + seriesId object, close
                    // this object when closing the last item of a superklipp serie
                    applyEffect(aid, EnkronaAuctions.Fade.curry(aid, { duration: 1 }));

                    if (auctionItem.isSuperklipp()) {
                        applyEffect(sid, EnkronaAuctions.BlindUp.curry(sid, { duration: 1, scaleContent: false }));
                    }
                    else {
                        applyEffect(aid, EnkronaAuctions.BlindUp.curry(aid, { duration: 1, scaleContent: false }));
                    }
                }
                else {
                    // there is a subsequent item in the series. Start it's clock and show it

                    nextAuctionItem.hasActiveClock = true;
                    this.updateClockObject(this, nextAuctionItem);

                    applyEffect(aid, EnkronaAuctions.BlindUp.curry(auctionObj.id, { duration: 1, scaleContent: false }));
                    applyEffect(aid, EnkronaAuctions.Fade.curry(auctionObj.id, { duration: 1 }));
                }
            },

            start: function () {
                if (this.updateExecuter !== null) {
                    this.updateExecuter.stop();
                }
                // superklipp auctions are pushed from soonest to furthest, but we want to pop the soonest items
                // using .shift() is to slow so we preprocess the lists instead
                var self = this;
                this.series.each(function (key_value) {
                    key_value[1].reverse();
                });
                this.auctionItems.each(function (key_value) {
                    // console.log('scheduling ' + key_value[1]);
                    self.scheduleClockUpdate(key_value[1]);
                    if ((navigator.userAgent.indexOf('Safari/') > -1) && !(navigator.userAgent.indexOf('Chrome/') > -1) && (typeof key_value[1].getBidObj().value !== 'undefined')) {
                        key_value[1].getBidObj().value = key_value[1].currentBid + " kr";
                        self.updateClockObject(self, key_value[1]);
                    }
                });
                this.getAuctionUpdates(this, true);
                this.updateExecuter = new PeriodicalExecuter(this.getAuctionUpdates.bindAsEventListener(this), this.interval);
                return this.updateExecuter;
            }
        })
    };
} ());
