base_url = "webAPI"

function showLoading()
{
    var imgsrc = '/public/images/loading.gif';
    if (!$("#loading_thing").length) {
        var loading = $('<div class="loading" id="loading_thing"><img src="%s"><br/>Loading, please wait...</div>'.format(imgsrc));
        $(document.body).append(loading);
    }
    var loading = $("#loading_thing");
    loading.fadeIn(200);
}

function hideLoading()
{
    var loading = $("#loading_thing");
    loading.fadeOut(200);
}

////////////////////////////////////////
///       RunnerCounter class        ///
////////////////////////////////////////
var RunnerCounter = RB.LWClass();
_p = RunnerCounter.prototype;

_p.initialize = function(stationList) {
    this.stationRecords = {};
    this.stationSeen = {};
    this.stationPredict = {};

    this.lastSeenAt = {};
    this.droppedAt = {};
    this.addEntry = this.$("addEntry");
    this.countSeenAtStation = this.$("countSeenAtStation");
    this.countExpected = this.$("countExpected");
    this.setStationList = this.$("setStationList");
    this.setDroppedAt = this.$("setDroppedAt");
    this.findLastSeen = this.$("findLastSeen");
    this.getLastSeen = this.$("getLastSeen");

    this.setStationList(stationList);
}

_p.setDroppedAt = function(bibNo, sNo) {
    if (!sNo && sNo !== 0) {
        if (this.droppedAt[bibNo]) {
            delete this.droppedAt[bibNo];
        }
    } else {
        this.droppedAt[bibNo] = sNo;
    }
}

_p.setStationList = function(stationList) {
    this.stationList = stationList;
    for (var i in stationList) {
        var sNo = stationList[i];
        if (!this.stationSeen[sNo]) {
            this.stationSeen[sNo] = 0;
        }
        if (!this.stationRecords[sNo]) {
            this.stationRecords[sNo] = {};
        }
    }
}

_p.seenAt = function(bibNo, sNo) {
    if (this.stationRecords[sNo] && this.stationRecords[sNo][bibNo]) {
        return this.stationRecords[sNo][bibNo].seen;
    } else {
        return false;
    }
}

_p.findLastSeen = function(bibNo) {
    var lastSeen = -1;
    for (var i = 0; i < this.stationList.length; i++) {
        var sNo = this.stationList[i];
        if (this.stationRecords[sNo][bibNo] && this.stationRecords[sNo][bibNo].seen) {
            lastSeen = sNo;
        }
    }
    return lastSeen;
}

_p.getLastSeen = function(bibNo) {
    if (this.lastSeenAt[bibNo]) {
        return this.lastSeenAt[bibNo];
    } else {
        return -1;
    }
}

_p.countExpected = function(sNo) {
    var count = 0;
    for (var bibNo in this.lastSeenAt) {
        var seenAt = this.lastSeenAt[bibNo];
        var droppedAt = this.droppedAt[bibNo] || 999;
        if (seenAt >= 0 && sNo > seenAt && sNo < droppedAt) {
            count++;
        }
    }
    return count;
}

_p.setSeen = function(bibNo, sNo, seen) {
    if (seen) {
        if (!this.lastSeenAt[bibNo] || this.lastSeenAt[bibNo] < sNo) {
            this.lastSeenAt[bibNo] = sNo;
        }
    } else {
        if (this.lastSeenAt[bibNo]) {
            this.lastSeenAt[bibNo] = this.findLastSeen(bibNo);
        }
    }
    if (!this.stationRecords[sNo][bibNo]) {
        this.stationRecords[sNo][bibNo] = {};
        if (seen) {
            // Add to the count
            this.stationSeen[sNo]++;
            this.stationRecords[sNo][bibNo].seen = 1;
        } else {
            this.stationRecords[sNo][bibNo].seen = 0;
        }
    } else {
        if (this.stationRecords[sNo][bibNo].seen && !seen) {
            // Remove from the count
            this.stationSeen[sNo]--;
            this.stationRecords[sNo][bibNo].seen = 0;
        } else if (!this.stationRecords[sNo][bibNo].seen && seen) {
            // Add to the count
            this.stationSeen[sNo]++;
            this.stationRecords[sNo][bibNo].seen = 1;
        }
    }
}

_p.addEntry = function(entry) {
    var bibNo = entry.participant.bibNumber;
    var sNo = entry.station_stationNumber;

    var seen = false;
    if (entry.time_in.value || entry.time_out.value) {
        seen = true;
    }
    this.setSeen.apply(this, [bibNo, sNo, seen]);
}

_p.countSeenAtStation = function(sNo) {
    return this.stationSeen[sNo];
}



////////////////////////////////////////
///        RaceControl class         ///
////////////////////////////////////////
var RaceControl = RB.LWClass();
_p = RaceControl.prototype;

_p.initialize = function() {
    this.events = {};
    this.latestSeen = {
        raceEvent: 0,
        participant: 0,
        station: 0,
        entry: 0,
        all: 0,
    };
};

_p.seen = function(key, val)
{
    if (!val) {
        return;
    }
    if (this.latestSeen[key] < val) {
        this.latestSeen[key] = val;
    }
}

_p.checkLoggedIn = function(ifYes, ifNo) {
    $.ajax({
        url: base_url + "/isLoggedIn",
        dataType: "json",
        error: RB.genericAjaxError,
        success: function(data, status) {
            if (data.authenticated) {
                if (ifYes) ifYes();
            } else if (ifNo) {
                ifNo();
            }
        },
    });
};

_p.LoginForm = function(modal, succeeded, failed) {
    var self = this;
    if (!modal) modal = false;
    var allowClose = false;

    var dialog = $('<div><div class="ui-state-error" id="loginError"></div><p></p></div>');
    var html  = '<ul><li><label>Username: </label><input type="text" name="username" placeholder="Username" /></li>';
        html += '<li><label>Password: </label><input type="password" name="password" placeholder="Password" /></li></ul>';
    dialog.append(html);
    dialog.append(RB.html.button("btnLogin", "Login", "check"));
    dialog.append(RB.html.button("btnCancel", "Cancel", "closethick"));

    dialog.find("#loginError").hide();

    dialog.dialog({
        title: "Please enter your username and password",
        autoOpen : true,
        modal: modal,
        dialogClass: 'loginDialog',
        resizable: false,
        minHeight: 40,
        closeOnEscape: false,
        open: function(event, ui)
        {
            RB.dialog.removeClose(event);
            dialog.find("input").keyup(function(e) {
                if (e.keyCode == 13) {
                    if ($(this).attr("name") == "username") {
                        dialog.find("input[name=password]").focus();
                    } else {
                        dialog.find("#btnAuthorize").click();
                    }
                }
            });
        }
    });

    dialog.TextfieldPlaceholders();
    dialog.find(".hover").hover(
            function() { $(this).addClass("ui-state-hover"); },
            function() { $(this).removeClass("ui-state-hover"); });

    dialog.find("#btnLogin").click(function() {
        dialog.find("#loginError").slideUp(200);
        var username = dialog.find("input[name=username]").val();
        var password = dialog.find("input[name=password]").val();
        self.$("Login")(username, password, function(data, status) {
            if (data.status == "success") {
                dialog.dialog("close");
                dialog.remove();
                if (succeeded) {
                    succeeded();
                }
            } else {
                dialog.find("#loginError").html(data.message).slideDown(500);
            }
        }, function(nil, status, error) {
            RB.alert("Error logging in:", "Error: %s (%s)".format(status, error));
        });
    });
    dialog.find("#btnCancel").click(function() {
        dialog.dialog("close");
        dialog.remove();
        if (failed) {
            failed();
        }
    });

    dialog.find("input[name=username]").focus();
}

_p.Login = function(username, password, CbFunc, ErrorFunc) {
    $.ajax({
        url : base_url + "/login",
        ifModified : false,
        data: { user: username, pass: password },
        dataType: "json",
        type: "GET",
        error : ErrorFunc,
        success : CbFunc
    });
}

_p.newRaceEvent = function(data) {
    return new RaceEvent(this, data);
}

_p.getRaceEvent = function(evtId, CbFunc) {
    var self = this;
    $.ajax({
        url : base_url + "/getFullEventData/%s".format(evtId),
        ifModified : false,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            var retVal = self.$("newRaceEvent")(data);
            self.latestSeen.all = data.LastUpdated;
            self.events[data.uuid] = retVal;
            CbFunc(retVal);
        },
    })
}

_p.getRaceEventResults = function(evtId, CbFunc) {
    var self = this;
    $.ajax({
        url : base_url + "/getEventResults/%s".format(evtId),
        ifModified : false,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            var retVal = self.$("newRaceEvent")(data);
            self.latestSeen.all = data.LastUpdated;
            self.events[data.uuid] = retVal;
            CbFunc(retVal);
        },
    })
}

_p.getRaceEvents = function(deep, CbFunc) {
    var self = this;
    $.ajax({
        url : base_url + "/getEventList",
        ifModified : false,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            retVal = {};
            for (var i in data) {
                retVal[data[i].uuid] = new RaceEvent(self, data[i]);
                self.events[data[i].uuid] = retVal;
                self.$("seen")("raceEvent", new Date(data[i].modified).getTime());
            }
            CbFunc(retVal);
        },
    })
}

_p.getUpdates = function(evtId, CbFunc) {
    var self = this;
    $.ajax({
        url : base_url + "/getAllUpdates",
        ifModified : false,
        dataType: "json",
        data: {evtId: evtId, modified: this.latestSeen.all},
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            CbFunc(data);
        },
    })
}

/********** Data model classes ***********/

////////////////////////////////////////
///         RaceEvent class          ///
////////////////////////////////////////
var RaceEvent = RB.LWClass(RB.DbModel);
RaceEvent.speedyInit = false;
_p = RaceEvent.prototype;

_p.initialize = function(system, data) {
    this.updateTimerId = -1;
    this.seconds = 5;
    this.urls.deleteUrl = base_url + "/deleteRaceEvent";
    this.urls.saveUrl   = base_url + "/updateRaceEvent";
    this.stationList = []; // Gives us a sorted list
    this.participantList = []; // Gives us a sorted list
    this.updateNotifiers = [];

    if (this.participants) {
        this.$("loadParticipants")(this.participants);
    } else {
        this.participants = {};
    }
    if (this.stations) {
        this.$("loadStations")(this.stations);
    } else {
        this.stations = {};
    }
};

_p.initFields = function() {
    this.$("addField")("date", new RB.DbFields.DateTimeField({max_length: 200, tableWidth: "20%", defaultVal:new Date(), verbose_name:"Event Date", center: true}));
    this.$("addField")("name", new RB.DbFields.CharField({max_length: 200, tableWidth: "70%", verbose_name:"Event Name", center: true}));
    this.$("addField")("startStation", new RB.DbFields.ForeignKeyField({verbose_name:"Start Station","_null":true,blank:true,fk_model:Station,fk_modelName:"Station",fk_selectField:"uuid"}));
    this.$("addField")("finishStation", new RB.DbFields.ForeignKeyField({verbose_name:"Finish Station","_null":true,blank:true,fk_model:Station,fk_modelName:"Station",fk_selectField:"uuid"}));
};

_p.watchForUpdates = function(callback, seconds) {
    if (!seconds) seconds = this.seconds;
    else this.seconds = seconds;
    if (this.updateTimerId > -1) {
        clearTimeout(this.updateTimerId);
    }
    if (callback) {
        this.updateNotifiers.push(callback);
    }
    this.updateTimerId = setTimeout(this.$("getUpdates"), seconds * 1000);
}

_p.stopUpdates = function() {
    if (this.updateTimerId > -1) {
        clearTimeout(this.updateTimerId);
        this.updateTimerId = -1;
    }
}

_p.updateNow = function() {
    this.$("stopUpdates")();
    this.$("getUpdates")();
}

_p.getUpdates = function(callback) {
    var self = this;
    this.system.$("getUpdates")(this.uuid.value, function(data) {
        self.system.latestSeen.all = data.LastUpdated;
        var refreshEvent = false;
        for (var i in data.RaceEvents) {
            var id = data.RaceEvents[i].uuid;
            if (self.uuid.value != id) {
                RB.alert("What???", "Something bad happened.  Error 1.4");
                return;
            } else {
                self.$("loadArray")(data.RaceEvents[i]);
                refreshEvent = true;
            }
        }

        self.$("loadParticipants")(data.Participants);
        var refreshStations = self.$("loadStations")(data.Stations);
        var refreshParticipants = data.Participants;
        var refreshEntries = data.Entrys;
        for (var i in data.Entrys) {
            var tmp = self.$("handleEntryUpdate")(data.Entrys[i]);
        }
        for (var i in self.updateNotifiers) {
            self.updateNotifiers[i]({event: refreshEvent, stations: refreshStations, participants: refreshParticipants, entries: refreshEntries }, this);
        }
        self.$("watchForUpdates")();
    });
}

_p.search = function(searchTerm, cbFunc) {
    data = {name: searchTerm, evtId: this.uuid.value};
    $.ajax({
        url : base_url + "/findByName",
        ifModified : false,
        data: data,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            cbFunc(data);
        },
    });
};

_p.getParticipantSummary = function(bibNo, cbFunc) {
    data = {bib: bibNo, evtId: this.uuid.value};
    $.ajax({
        url : base_url + "/getParticipant",
        ifModified : false,
        data: data,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            cbFunc(data);
        },
    });
};

_p.sendMassEntryUpdates = function(entryList, callback) {
    var self = this;
    var sendData = [];
    for (var i = 0; i < entryList.length; i++) {
        var e = entryList[i];
        var curEntry = e.serializeToObject();
        curEntry.raceEvent_id = this.uuid.value;
        curEntry.stationNumber = e.station_stationNumber;
        curEntry.bibNumber = e.participant.bibNumber.value;
        sendData.push(curEntry);
    }

    this.stopUpdates();
    var data = $.toJSON(sendData);
    $.ajax({
        url : base_url + "/updateEntry",
        ifModified : false,
        data: data,
        dataType: "json",
        type: "POST",
        error : RB.genericAjaxError,
        contentType: "application/json",
        processData: false,
        success : function(data, status) {
            self.updateNow();
            if (data.status == "failed") {
                if (data.message == "Unauthenticated") {
                    self.system.$("LoginForm")(true, function() {
                        self.$("save")(callback);
                    }, function() {
                        if (callback) {
                            callback(false, "Unauthorized");
                        }
                    });
                } else {
                    if (callback) {
                        callback(false, data.message);
                    }
                }
            } else {
                if (callback) {
                    callback(true, self);
                }
            }
        },
    });
}

_p.loadParticipants = function(plist) {
    var src = $.extend(true, {}, plist);
    var retVal = false;
    for (var i in src) {
        retVal = true;
        var p = src[i];
        var pNo = p.bibNumber;
        if (typeof(pNo) == "object") {
            pNo = pNo.value;
        }
        if (!this.participants[pNo] || !this.participants[pNo].loadArray) {
            this.newParticipant(pNo, src[i]); // Create if it doesn't exist or isn't a Participant object
        } else {
            this.participants[pNo].$("loadArray")(src[i]);
        }
    }
    return retVal;
}

_p.newParticipant = function(rNo, data) {
    var settings = $.extend({
        raceEvent_id: this.uuid.value,
        bibNumber: rNo,
        firstName: "Unknown",
        lastName: "Participant",
    }, data);
    if (!this.participants[rNo] || !this.participants[rNo].loadArray) {
        this.participants[rNo] = new Participant(this.system, settings);
        this.participantList.push(parseInt(rNo));
        this.participantList.sort(RB.sort.numericAsc);
    }
    return this.participants[rNo];
}

_p.loadStations = function(slist) {
    var src = $.extend(true, {}, slist);
    var retVal = false;
    for (var i in src) {
        var s = src[i];
        var sNo = s.stationNumber;
        if (typeof(sNo) == "object") {
            sNo = sNo.value;
        }
        retVal = true;
        if (!this.stations[sNo] || !this.stations[sNo].loadArray) {
            this.$("newStation")(sNo, src[i]);
        } else {
            this.stations[sNo].loadArray(src[i]);
        }
    }
    return retVal;
}

_p.newStation = function(stNo, data) {
    var settings = $.extend({
        raceEvent_id: this.uuid.value,
        stationNumber: stNo,
    }, data);
    if (!this.stations[stNo] || !this.stations[stNo].loadArray) {
        this.stations[stNo] = new Station(this.system, settings);
        this.stationList.push(parseInt(stNo));
        this.stationList.sort(RB.sort.numericAsc);
    }
    return this.stations[stNo];
}

_p.cancelStation = function(stNo) {
    if (this.stations[stNo] && this.stations[stNo].uuid.value == null) {
        delete this.stations[stNo];
        this.stationList.splice(this.stationList.indexOf(stNo), 1);
    }
}

_p.findStationById = function(stId) {
    for (stNo in this.stations) {
        if (this.stations[stNo].uuid.value == stId)
            return this.stations[stNo];
    }
    return null;
}

_p.handleEntryUpdate = function(data) {
    for (var rno in this.participants) {
        if (this.participants[rno].uuid.value == data.participant_id) {
            var tmpData = {};
            tmpData[data.station_stationNumber] = data;
            this.participants[rno].$("loadEntrys")(tmpData);
        }
    }
}

_p.getConfirmDeleteMessage = function() {
    return 'Are you sure you want to delete the event "%s" on %s?'.format(this.name, this.date);
};

_p.viewResults = function() {
    $(document.location).attr("href", "/event/results?evt=%s".format(this.uuid.value));
};

_p.lookup = function() {
    $(document.location).attr("href", "/lookup?evt=%s".format(this.uuid.value));
};

_p.open = function() {
    $(document.location).attr("href", "/event/main?evt=%s".format(this.uuid.value));
};

_p.showHelp = function() {
    var dialog = $("<div></div>");

    var helpText = "<p>Hotkeys that can be used with Speedy Entry time fields:</p>";

    helpText += "<ul style='text-align: left;'>";

    helpText += "<li>t or *: Current Time (HH:mm)</li>";
    helpText += "<li>r or /: Repeat last entered time</li>";
    helpText += "<li>T: Current Time w/ seconds (HH:mm:ss)</li>";
    helpText += "<li>a: Select all in field</li>";
    helpText += "<li>m: Select minutes in field</li>";
    helpText += "<li>s: Select seconds in field</li>";
    helpText += "<li>h: Select hours in field</li>";
    helpText += "<li>+: Add 1 minute to time</li>";
    helpText += "<li>-: Subtract 1 minute from time</li>";
    helpText += "<li>[DEL]: Erase current value</li>";

    helpText += "</ul>";

    dialog.append(helpText);
    dialog.dialog({
        title: "Speedy Entry Help",
        autoOpen: true,
        modal: true,
        close: function() { dialog.remove(); },
    });
}

_p.speedyEntry = function(opts) {
    var options = $.extend({
        onSave: function() {}
    }, opts);

    var gotoSpeedy = function(evt) {
        var char = evt.data;
        var n = parseInt(char.substring(char.length-1)); // alt+n
        n--;
        if (n < 0) n = 10; // map 0 to the 10th
        var speedy = $(".speedy:eq(%s)".format(n));
        speedy.dialog("moveToTop");
        if (speedy.find("#speedyTableHeader").attr("state") == "closed") {
            speedy.find("#txtSpeedyStation").select().focus();
        } else {
            speedy.find(".inputRow input[name=runNo]").select().focus();
        }
        evt.preventDefault();
    };
    var bindSpeedyShortcuts = function(elem) {
        elem.bind("keydown", "alt+1", gotoSpeedy);
        elem.bind("keydown", "alt+2", gotoSpeedy);
        elem.bind("keydown", "alt+3", gotoSpeedy);
        elem.bind("keydown", "alt+4", gotoSpeedy);
        elem.bind("keydown", "alt+5", gotoSpeedy);
        elem.bind("keydown", "alt+6", gotoSpeedy);
        elem.bind("keydown", "alt+7", gotoSpeedy);
        elem.bind("keydown", "alt+8", gotoSpeedy);
        elem.bind("keydown", "alt+9", gotoSpeedy);
        elem.bind("keydown", "alt+0", gotoSpeedy);
    }
    if (!RaceEvent.speedyInit) {
        RaceEvent.speedyInit = true;
        bindSpeedyShortcuts($(document));
    }

    var self = this;
    var dialog = $("<div></div>");
    var curEntry;

    dialog.append('<label for="station">Station number:</label> <input type="text" name="station" id="txtSpeedyStation" maxlength="2"  />');
    dialog.append("<a href='' style='float: right' class='helpLink'>Help</a>");
    var speedyTable = '<div id="speedyTable" style="padding-top: 0.5em;">';
        speedyTable+= '    <div class="ui-widget-header ui-corner-top ui-state-default" id="speedyTableHeader" state="closed">';
        speedyTable+= '        <ul>'
        speedyTable+= '<li class="num">S #</li>'
        speedyTable+= '<li class="num">R #</li>'
        speedyTable+= '<li class="name">Name</li>'
        speedyTable+= '<li class="station">Time In:</li>'
        speedyTable+= '<li class="station">Time Out:</li>'
        speedyTable+= '<li class="num last"><span class="ui-icon ui-icon-pencil small"></span></li>'
        speedyTable+= '</ul>';
        speedyTable+= '    </div>';
        speedyTable+= '    <div class="ui-widget-content ui-corner-bottom" id="speedyTableBody">';
        speedyTable+= '        <ul style="display: none;" class="inputRow"><li class="num stNo"></li><li class="num"><input type="text" class="numeric" value="" maxlength=3 name="runNo" /></li><li class="name"></li><li class="station timeIn"></li><li class="station timeOut last"></li><li class="num last"><span class="ui-icon ui-icon-disk small"></span></li></ul>';
        speedyTable+= '        <div id="speedyTableHistory">';
        speedyTable+= '        </div>';
        speedyTable+= '    </div>';
        speedyTable+= '</div>';

    dialog.find("a.helpLink").click(function(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        self.showHelp();
    });

    speedyTable = $(speedyTable);
    bindSpeedyShortcuts(dialog.find("input"));

    dialog.append(speedyTable);

    var cancel = function() {
        dialog.find(".inputRow").hide();
        dialog.find("input[name=station]").focus().select();
        dialog.find("#speedyTableHeader").attr("state", "closed");
        if (curEntry)
            curEntry.$("cancel")();
    }

    dialog.bind("keydown", "esc", cancel);
    dialog.find("input").bind("keydown", "esc", cancel);

    var addInputRow = function() {
        var stNo = parseInt(dialog.find("input[name=station]").val());
        if (isNaN(stNo)) {
            RB.alert("Please enter a valid number for the station");
            dialog.find("input[name=station]").focus().select();
        }
        if (!self.stations[stNo]) {
            self.$("newStation")(stNo);
            self.stations[stNo].$("edit")(function(success, message) {
                if (success) {
                    options.onSave();
                    addInputRow();
                } else {
                    RB.alert("Could not add station", message);
                    cancel();
                }
            }, function() {
                self.$("cancelStation")(stNo);
                cancel();
            });
            return;
        }
        var station = self.stations[stNo];
        dialog.find("#speedyTableHeader").attr("state", "open");
        dialog.dialog("option", "title", "Speedy Entry Form for %s: %s".format(stNo, station.name));

        dialog.find(".inputRow").show();
        dialog.find("input[name=runNo]").css("background-color", "#fff").attr("readonly", false);
        dialog.find(".inputRow .stNo").html(stNo);
        dialog.find(".inputRow .station").html("");
        dialog.find(".inputRow .name").html("");
        dialog.find("input[name=runNo]").val("").focus();
    };

    var saveRow = function() {
        var participant = curEntry.participant;
        var time_in = curEntry.time_in.$("fromForm")(dialog);
        if (time_in === undefined) {
            return;
        }
        var time_out = curEntry.time_out.$("fromForm")(dialog);
        if (time_out === undefined) {
            return;
        }
        curEntry.time_in.$("set")(time_in);
        curEntry.time_out.$("set")(time_out);
        curEntry.$("save")(function(success, msg) {
            if (!success) {
                RB.alert("Could not save", msg);
                return;
            }
            var newRow = $("<ul></ul>");
            newRow.append('<li class="num stNo">%s</li>'.format(dialog.find(".stNo").html()));
            newRow.append('<li class="num">%s</li>'.format(participant.bibNumber.$("toString")()));
            newRow.append('<li class="name">%s, %s</li>'.format(participant.lastName, participant.firstName));
            newRow.append('<li class="station">%s</li>'.format(curEntry.time_in.$("toTimeString")() || "__:__"));
            newRow.append('<li class="station">%s</li>'.format(curEntry.time_out.$("toTimeString")() || "__:__"));
            newRow.append('<li class="num last"><span class="ui-icon ui-icon-pencil small"></span></li>');
            dialog.find("#speedyTableHistory").prepend(newRow);
            options.onSave();
            cancel();
            addInputRow();
        });
    }

    var timeEnter = function() {
        if ($(this).attr("name") == "fld_time_in") {
            $(this).blur();
            if (dialog.find("input[name=fld_time_out]").attr("disabled")) {
                saveRow();
            } else {
                dialog.find("input[name=fld_time_out]").focus();
            }
        } else if ($(this).attr("name") == "fld_time_out") {
            $(this).blur();
            saveRow();
        }
    }

    var selectParticipant = function() {
        var stNo = dialog.find(".inputRow .stNo").html();
        var rNo = dialog.find("input[name=runNo]").val();
        if (rNo.length < 1 || isNaN(rNo)) {
            return;
        }
        dialog.find(".inputRow .station").html("");
        dialog.find(".inputRow .name").html("");
        if (isNaN(rNo)) return;
        dialog.find("input[name=runNo]").css("background-color", "#ddd").attr("readonly", true);
        var participant = self.participants[rNo];
        if (!participant) {
            participant = self.newParticipant(rNo);
        }
        var entry = participant.entrys[stNo];
        if (!entry) {
            entry = participant.newEntry(stNo);
        }
        var timeIn_disabled = stNo == self.startStation.value;
        var timeOut_disabled = stNo == self.finishStation.value;
        dialog.find(".inputRow .name").html("%s, %s".format(participant.lastName, participant.firstName));
        dialog.find(".inputRow .timeIn").append(entry.time_in.$("makeHtml")({date: false, class:"timeField time_entry", disabled: timeIn_disabled}));
        dialog.find(".inputRow .timeOut").append(entry.time_out.$("makeHtml")({date: false, class:"timeField time_entry", disabled: timeOut_disabled}));
        var tmp = dialog.find(".time_entry");
        tmp.bind("keydown", "return", timeEnter);
        tmp.bind("keydown", "esc", cancel);
        if (entry.time_in.isNull() && !timeIn_disabled) {
            dialog.find("input[name=fld_time_in]").focus();
        } else if (entry.time_out.isNull() && !timeOut_disabled) {
            dialog.find("input[name=fld_time_out]").focus();
        } else if (!timeIn_disabled){
            dialog.find("input[name=fld_time_in]").focus();
        } else {
            dialog.find("input[name=fld_time_out]").focus();
        }
        curEntry = entry;
    }

    dialog.find("#txtSpeedyStation").inputField({
        allowNumeric: true,
        allowAlpha: false,
        disallowChars: ".",
        keyTriggers: {"return": addInputRow}
    });

    dialog.find("input[name=runNo]").inputField({
        allowNumeric: true,
        allowAlpha: false,
        disallowChars: ".",
        keyTriggers: {"return": selectParticipant},
    });

    dialog.dialog({
        title: "Speedy Entry form",
        autoOpen : true,
        modal: false,
        dialogClass: 'stdTablePage grid speedy',
        resizable: true,
        minHeight: 200,
        minWidth: 420,
        width: 420,
        closeOnEscape: false,
        close: function() { dialog.remove(); },
        open: function() {
            dialog.find("#txtSpeedyStation").focus().select();
        },
    });
};

_p.quickLookup = function(opts) {
    var options = opts;

    var gotoQuick = function(evt) {
        var char = evt.data;
        var n = parseInt(char.substring(char.length-1)); // alt+shift+n
        n--;
        if (n < 0) n = 10; // map 0 to the 10th
        var quick = $(".quick:eq(%s)".format(n));
        quick.dialog("moveToTop");
        quick.find("#txtQuickSearch").select().focus();
        evt.preventDefault();
    };
    var bindQuickShortcuts = function(elem) {
        elem.bind("keydown", "alt+shift+1", gotoQuick);
        elem.bind("keydown", "alt+shift+2", gotoQuick);
        elem.bind("keydown", "alt+shift+3", gotoQuick);
        elem.bind("keydown", "alt+shift+4", gotoQuick);
        elem.bind("keydown", "alt+shift+5", gotoQuick);
        elem.bind("keydown", "alt+shift+6", gotoQuick);
        elem.bind("keydown", "alt+shift+7", gotoQuick);
        elem.bind("keydown", "alt+shift+8", gotoQuick);
        elem.bind("keydown", "alt+shift+9", gotoQuick);
        elem.bind("keydown", "alt+shift+0", gotoQuick);
    }
    if (!RaceEvent.quickInit) {
        RaceEvent.quickInit = true;
        bindQuickShortcuts($(document));
    }

    var self = this;
    var evtID = this.uuid.value;
    var dialog = $("<div></div>");

    var cancel = function() {
        dialog.find("#txtQuickSearch").focus().select();
        dialog.find("#searchResults").html("");
    }

    var performSearch = function(stations) {
        var searchTerm = $.trim(dialog.find("input[name=search]").val());
        dialog.find(".searchResults").html('');

        self.$("search")(searchTerm, function(list) {
            if (list.length == 0) {
                dialog.find("#searchResults").html("No results found");
            } else {
                var html = '<div class="ui-widget %s">'.format("searchResults");
                html += '    <div class="ui-widget-html ui-corner-top ui-state-default" id="gridhtml">';
                html += '        <span>%s results found</span>'.format(list.length);
                html += '<ul><li class="num">Bib#</li>'
                html += '<li class="name">Name</li>'
                html += '<li class="from last">From</li></ul>'
                html += '    </div>';
                html += '    <div class="ui-widget-content ui-corner-bottom resultContainer" id="resultContainer">';
                for (var i = 0; i < list.length; i++) {
                    var l = list[i];
                    html += '<ul rNo="%s"><li class="num">%s</li>'.format(l.bibNumber, l.bibNumber);
                    html += '<li class="name">%s %s</li>'.format(l.firstName, l.lastName);
                    html += '<li class="from last">%s</li></ul>'.format(l.home);
                }
                html += '    </div>';
                dialog.find("#searchResults").html(html);

                dialog.find("#searchResults ul").click(function(evt) {
                    var bibNo = $(this).attr("rNo");
                    self.$("showParticipantSummary")(bibNo, stations);
                });
            }
        });
    };

    Station.getNumbersAndNames(evtID, function(stations){
        dialog.append('<label for="txtQuickSearch">Search for:</label> <input type="text" name="search" id="txtQuickSearch" />');
        dialog.append('<div id="searchResults"></div>');

        bindQuickShortcuts(dialog.find("input"));

        dialog.bind("keydown", "esc", cancel);
        dialog.find("input").bind("keydown", "esc", cancel);
        dialog.find("#txtQuickSearch").bind("keydown", "return", function() {performSearch(stations);});
    });

    dialog.dialog({
        title: "Quick Search form",
        autoOpen : true,
        modal: false,
        dialogClass: 'stdTablePage',
        resizable: true,
        minHeight: 200,
        minWidth: 420,
        width: 420,
        closeOnEscape: false,
        close: function() { dialog.remove(); },
        open: function() {
            dialog.find("#txtQuickSearch").focus().select();
        },
    });
};

_p.findAndReplace = function(opts) {
    var options = opts;
    var self = this;
    var dialog = $("<div></div>");
    var txtFind = new RB.DbFields.TimeField({blank:true});
    txtFind.$("setName")("find");
    var txtReplace = new RB.DbFields.TimeField({blank:true});
    txtReplace.$("setName")("replace");

    var html = "<ul>";
    html += '<li><label for="selStation">Station:</label><select name="selStation">';
    for (var i = 0; i < this.stationList.length; i++) {
        var station = this.stations[this.stationList[i]];
        html += '<option value="%s">%s</option>'.format(station.stationNumber.value, station.name.value);
    }
    html += "</select></li>";
    html += '<li><label for="selStation">Time:</label><select name="selTime">';
    html += '<option value="time_in">Time in</option>';
    html += '<option value="time_out">Time out</option>';
    html += "</select></li></ul>";
    var form = $(html);

    html = $('<li><label for="txtFind">Find:</label></li>');
    html.append(txtFind.$("makeHtml")());
    form.append(html);
    html = $('<li><label for="txtReplace">Replace:</label></li>');
    html.append(txtReplace.$("makeHtml")());
    form.append(html);

    dialog.append(form);

    var cancel = function() {
        dialog.dialog("close");
        dialog.remove();
    }

    var doReplace = function() {
        var find = txtFind.$("fromForm")(dialog);
        var replace = txtReplace.$("fromForm")(dialog);
        var sNo = dialog.find("select[name=selStation]").val();
        var timeType = dialog.find("select[name=selTime]").val();

        var updateList = [];

        if (!find) find = "";
        else find = find.toString("HH:mm:ss");
        if (!replace) replace = "";
        else replace = replace.toString("HH:mm:ss");

        var getTime = function(entry, type) {
            if (entry && entry[type].value) {
                return entry[type].value.toString("HH:mm:ss");
            } else {
                return "";
            }
        }
        var setTime = function(p, sNo, type, newTime) {
            var entry = p[sNo];
            if (!entry) {
                entry = p.$("newEntry")(sNo);
                entry.station_stationNumber = sNo;
            }
            entry[type].set(newTime);
        }

        for (var bib in self.participants) {
            var p = self.participants[bib];
            var s = getTime(p.entrys[sNo], timeType);

            var n = s == find;
            if (s == find) {
                setTime(p, sNo, timeType, replace);
                updateList.push(p.entrys[sNo]);
            }
        }
        self.$("sendMassEntryUpdates")(updateList, function() {
            self.updateNow();
            dialog.dialog("close");
            dialog.remove();
        });
    }

    dialog.dialog({
        title: "Quick Search form",
        autoOpen : true,
        modal: false,
        dialogClass: 'quickSearch',
        resizable: true,
        minHeight: 150,
        minWidth: 320,
        width: 320,
        buttons: {
            Go: doReplace,
            Cancel: cancel,
        },
        closeOnEscape: false,
        close: function() { dialog.remove(); },
        open: function(evt) {
            dialog.find("select[name=selStation]").focus().select();
            RB.dialog.setButtonIcon(evt, "Go", "shuffle");
            RB.dialog.setButtonIcon(evt, "Cancel", "closethick");
        },
    });

}

_p.showParticipantSummary = function(bibNo, stations) {
    var self = this;
    this.$("getParticipantSummary")(bibNo, function(participant) { self.$("showParticipantSummary_loaded")(participant, stations); } );
}

_p.showParticipantSummary_loaded = function(participant, stations) {
    var self = this;
    var p = participant;
    var dialog = $('<div></div>');

    var html = "";
    if (!p.entrys[this.startStation.value] || !p.entrys[this.startStation].time_out) {
        html += "We have no record of %s %s starting this race.  Check back later.".format(p.firstName, p.lastName);
    } else {
        var start = new Date(p.entrys[this.startStation].time_out);
        html  = '<div class="ui-widget %s">'.format("participantSummary");
        html += '    <div class="ui-widget-html ui-corner-top ui-state-default" id="gridhtml">';
        html += '        <span>Participant Details</span>';
        html += '<ul><li class="col-header">Bib #:</li><li class="ui-widget-content not-header">' + p.bibNumber + '</li>';
        html += '<li class="col-header">Age:</li><li class="ui-widget-content not-header">' + p.age + '</li>';
        html += '<li class="col-header">Sex:</li><li class="ui-widget-content not-header">' + p.sex + '</li>';
        html += '<li class="col-header">Home:</li><li class="ui-widget-content not-header">' + p.home + '</li>';
        html += '<li class="col-header">Team:</li><li class="ui-widget-content not-header">' + (p.team ? p.team : 'None') + '</li></ul>';
        html += '    </div>';
        html += '    <div class="ui-widget-content ui-corner-bottom">';
        if (p.dnfStation !== null)
            html += '       <ul><li class="droppedat">Dropped at station ' + p.dnfStation + ': ' + p.dnfReason + '</li></ul>';
        html += '    </div>';
        html += '    <br/>';
        html += '    <div class="ui-widget-html ui-corner-top ui-state-default" id="gridhtml">';
        html += '        <span>Reported Sightings</span>';
        html += '<ul><li class="num">#</li>';
        html += '<li class="name">Name</li>';
        html += '<li class="station">Distance</li>';
        html += '<li class="station">Arrived</li>';
        html += '<li class="station last">Left</li></ul>';
        html += '    </div>';
        html += '    <div class="ui-widget-content ui-corner-bottom resultContainer" id="resultContainer">';
        for (var sNo in stations) {
            var t_in;
            var t_out;
            if (sNo in p.entrys)
            {
                var e = p.entrys[sNo];
                t_in = Date.parse(e.time_in);
                t_out = Date.parse(e.time_out);
            } else {
                t_in = null;
                t_out = null;
            }

            html += '<ul sNo="%s"><li class="num">%s</li>'.format(sNo, sNo);
            html += '<li class="name">%s</li>'.format(stations[sNo].name);
            html += '<li class="station">%s</li>'.format(stations[sNo].distance || "&nbsp;");

            var arrived;
            if (sNo == this.startStation) {
                arrived = "START";
            } else if (!t_in) {
                arrived = "No Record";
            } else {
                arrived = t_in.toString('HH:mm:ss');
            }

            var left;
            if (sNo == this.finishStation) {
                left = "FINISH";
            } else if (!t_out) {
                left = "No Record";
            } else {
                left = t_out.toString('HH:mm:ss');
            }
            html += '<li class="station">%s</li>'.format(arrived);
            html += '<li class="station last">%s</li></ul>'.format(left);
        }
        html += '    </div>';
        html += '    </div>';
    }
    html += "<br/><br/>"
    dialog.append(html);

    var btn = $("<button name='refresh'>Refresh</button>")
        .buttonize("arrowrefresh-1-e")
        .click(function() { self.$("showParticipantSummary")(p.bibNumber, stations); dialog.dialog("close"); dialog.remove(); });
    var btn2 = $("<button name='close'>Close</button>")
        .buttonize("closethick")
        .click(function() { self.$("showParticipantSummary")(p.bibNumber, stations); dialog.dialog("close"); dialog.remove(); });

    dialog.append(btn);

    dialog.dialog({
        title: "%s: %s %s".format(p.bibNumber, p.firstName, p.lastName),
        autoOpen : true,
        modal: false,
        dialogClass: 'stdTablePage',
        resizable: true,
        minHeight: 200,
        minWidth: 500,
        width: 500,
        closeOnEscape: false,
        close: function() { dialog.remove(); },
        open: function() {
            dialog.find("#txtQuickSearch").focus().select();
        },
    });
}
////////////////////////////////////////
///          Station class           ///
////////////////////////////////////////
var Station = RB.LWClass(RB.DbModel);

Station.getList = function(selectVal, cbFunc) {
    data = {select: selectVal};
    $.ajax({
        url : base_url + "/getStationList",
        ifModified : false,
        data: data,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            var list = {};
            for (var i in data) {
                list[data[i].stationNumber] = "%s: %s".format(data[i].stationNumber, data[i].name);
            }
            cbFunc(list);
        },
    });
};

Station.getNumbersAndNames = function(selectVal, cbFunc) {
    data = {select: selectVal};
    $.ajax({
        url : base_url + "/getStationList",
        ifModified : false,
        data: data,
        dataType: "json",
        type: "GET",
        error : RB.genericAjaxError,
        success : function(data, status) {
            var list = {};
            for (var i in data) {
                list[data[i].stationNumber] = {'name': data[i].name, 'distance': data[i].distance};
            }
            cbFunc(list);
        },
    });
};

_p = Station.prototype;

_p.initialize = function(system, data) {
    this.urls.deleteUrl = base_url + "/deleteStation";
    this.urls.saveUrl   = base_url + "/updateStation";
};

_p.initFields = function() {
    this.$("addField")("raceEvent_id", new RB.DbFields.UuidField({hidden: true}));
    this.$("addField")("name", new RB.DbFields.CharField({max_length: 200,verbose_name:"Station Name"}));
    this.$("addField")("stationNumber", new RB.DbFields.IntegerField({verbose_name:"Station Number"}));
    this.$("addField")("distance", new RB.DbFields.DecimalField({verbose_name:"Mile on Course", nullval: ""}));
}

_p.getConfirmDeleteMessage = function() {
    return 'Are you sure you want to delete station "%s"?'.format(this.name.$("toString")());
}

_p.getEvent = function() {
    return this.system.events[this.raceEvent_id.value];
}

_p.save = function(callback) {
    var self = this;
    this.$("getEvent")().$("stopUpdates")();
    this.super.$("save")(function() {
            self.$("getEvent")().watchForUpdates();
        callback.apply(this, arguments);
    })
}

////////////////////////////////////////
///        Participant class         ///
////////////////////////////////////////
var Participant = RB.LWClass(RB.DbModel);
_p = Participant.prototype;

_p.initialize = function(system, data) {
    this.urls.deleteUrl = base_url + "/deleteParticipant";
    this.urls.saveUrl   = base_url + "/updateParticipant";

    if (this.entrys) {
        this.$("loadEntrys")(this.entrys);
    } else {
        this.entrys = {};
    }
};

_p.loadEntrys = function(elist) {
    var retVal = false;
    for (var i in elist) {
        retVal = true;
        this.system.$("seen")("entry", new Date(elist[i].modified).getTime());
        if (this.entrys[i] && this.entrys[i].loadArray) {
            this.entrys[i].$("loadArray")(elist[i]);
        } else {
            this.entrys[i] = this.$("newEntry")(i, elist[i]);
        }
    }
    return retVal;
}

_p.newEntry = function(sNo, data) {
    var fields = $.extend({
        participant_id: this.uuid.value,
        time_in: null,
        time_out: null,
    }, data);
    if (!fields.station_id) {
        fields.station_id = this.getEvent().stations[sNo].uuid.value;
    }
    this.entrys[sNo] = new Entry(this.system, fields, this);
    return this.entrys[sNo];
}

_p.initFields = function() {
    var SEX_CHOICES = {
        M: 'Male',
        F: 'Female',
    };
    this.$("addField")("raceEvent_id", new RB.DbFields.UuidField({hidden: true}));
    this.$("addField")("bibNumber", new RB.DbFields.IntegerField({verbose_name: "Bib #", readonly: true}));
    this.$("addField")("firstName", new RB.DbFields.CharField({verbose_name: "First Name", max_length: 200}));
    this.$("addField")("lastName", new RB.DbFields.CharField({verbose_name: "Last Name", max_length: 200}));
    this.$("addField")("home", new RB.DbFields.CharField({verbose_name: "From Where", max_length: 200}));
    this.$("addField")("age", new RB.DbFields.IntegerField({verbose_name: "Age", }));
    this.$("addField")("sex", new RB.DbFields.CharField({verbose_name: "Sex", max_length: 2, choices:SEX_CHOICES}));
    this.$("addField")("team", new RB.DbFields.CharField({verbose_name: "Team", max_length: 2, blank:true}));
    this.$("addField")("dnfStation", new RB.DbFields.ForeignKeyField({verbose_name:"DNF Station","_null":true,blank:true,fk_model:Station,fk_modelName:"Station",fk_selectField:"raceEvent_id"}));
    this.$("addField")("dnfReason", new RB.DbFields.CharField({verbose_name: "DNF Reason", max_length: 20,"null":true,blank:true}));
}

_p.toString = function() {
    return "%s: %s %s".format(this.bibNumber.$("toString")(), this.firstName.$("toString")(), this.lastName.$("toString")());
}

_p.getConfirmDeleteMessage = function() {
    return 'Are you sure you want to delete participant "%s %s"?'.format(this.$("firstName").toString(), this.$("lastName").toString());
}

_p.save = function(callback) {
    this.getEvent().stopUpdates();
    var self = this;
    this.super.$("save")(function(success, data) {
        if (success) {
            for (var i in self.entrys) {
                self.entrys[i].participant = self;
                self.entrys[i].participant_id.$("set")(self.uuid.value);
            }
        }
        callback.apply(this, arguments);
        self.$("getEvent")().$("watchForUpdates")();
    });
}

_p.getEvent = function() {
    return this.system.events[this.raceEvent_id.value];
}

////////////////////////////////////////
///           Entry class            ///
////////////////////////////////////////
var Entry = RB.LWClass(RB.DbModel);
_p = Entry.prototype;

_p.initialize = function(system, data, participant) {
    this.participant = participant;
    this.urls.deleteUrl = base_url + "/deleteEntry";
    this.urls.saveUrl   = base_url + "/updateEntry";
};

_p.initFields = function() {
    this.$("addField")("participant_id", new RB.DbFields.UuidField({hidden: true}));
    this.$("addField")("station_id", new RB.DbFields.UuidField({hidden: true}));
    this.$("addField")("time_in", new RB.DbFields.DateTimeField({blank:true,verbose_name: "Time in"}));
    this.$("addField")("time_out", new RB.DbFields.DateTimeField({blank:true,verbose_name: "Time out"}));
}

_p.toString = function() {
    var tIn = this.time_in.$("toString")("HH:mm");
    if (!tIn) tIn = "__:__";
    var tOut = this.time_out.$("toString")("HH:mm");
    if (!tOut) tOut = "__:__";
    return "%s->%s".format(tIn, tOut);
}

_p.getConfirmDeleteMessage = function() {
    return 'Are you sure you want to delete participant "%s %s"?'.format(this.firstName.toString(), this.lastName.toString());
}

_p.save = function(callback) {
    var self = this;
    this.participant.$("getEvent")().$("stopUpdates")();
    if (!this.participant.uuid.value) {
        this.participant.$("save")(function(success, message) {
            if (success) {
                self.super.$("save")(callback);
            } else {
                callback(false, "Could not save new participant: %s".format(message));
            }
            self.participant.$("getEvent")().$("watchForUpdates")();
        });
    } else {
        this.super.$("save")(callback);
    }
}

_p.serializeToObject = function() {
    var output = {};
    for (var i in this.fieldList) {
        output[this.fieldList[i]] = this[this.fieldList[i]].$("toDatabase")();
    }
    output["participant_bibNumber"] = this.participant.bibNumber.$("toDatabase")();
    var evt = this.participant.getEvent();
    output["station_stationNumber"] = evt.findStationById(this.station_id.value).stationNumber.$("toDatabase")();
    output["raceEvent_id"] = evt.uuid.value;
    return output;
}

_p.cancel = function() {
    if (!this.uuid.value) {
        if (this.stationNumber) {
            delete this.participant.entries[this.stationNumber.value];
        }
    }
    if (!this.participant.uuid.value) {
        delete this.participant.$("getEvent")().participants[this.participant.bibNumber];
    }
}

