nf.UI = {

Pager: function(owner) {
    this.owner = owner;
    this.current = 1;
    this.total = 1;
},

Actions: function(owner) {
    this.owner = owner || window;
    this.items = {};
    this.rootElement = null;
    this.searchField = null;
},

Labels: {
    create: 'Create',
    createAll: 'Create All',
    search: 'Search',
    accept: 'Accept',
    unpair: 'Unpair',
    cancel: 'Cancel',
    acceptAll: 'Accept all suggestions',
    goToFirst: String.fromCharCode(0xAB),           // looks like <<
    goToLast: String.fromCharCode(0xBB),            // looks like >>
    goToPrevious: String.fromCharCode(0x2039),      // looks like <
    goToNext: String.fromCharCode(0x203A),  // looks like >
    of: 'of'
},

Tests: {

    email: function(a, b) {

        // direct comparison (case sensitive)
        if(a == b) {
            return 1;
        }

        // go case-insensitive
        var x = (a || '').toLowerCase(),
            y = (b || '').toLowerCase();

        // direct comparision
        if(x == y) {
            return .95;
        }

        // compare without domain suffixes
        if(x.replace(arguments.callee.variation, '') ==
            y.replace(arguments.callee.variation, '')) {
            return .85;
        }

        // use deprioritised string comparison
        return nf.UI.Tests.string(a, b) / 3;
    },

    fullName: function(a, b) {

        a = (a || '').trim();
        b = (b || '').trim();

        // direct comparison

        if(a == b) {
            return 1;
        }
        else if(a.toLowerCase() == b.toLowerCase()) {
            return .9;
        }

        // go to word by word

        var p = [a, b].map(function(x) {
            return x
                .trim()
                .split(/[^a-z]+/i)
                .filter(function(y) {
                    return y && nf.UI.Tests.fullName.salutations.indexOf(y.toLowerCase()) == -1;
                });
        });

        var score = 0, i, j, k = p[0].length, l = p[1].length, x, y;

        for(i = 0; i < k; i++) {
            for(j = 0; j < l; j++) {

                x = p[0][i]; // word from a
                y = p[1][j]; // word from b

                // whole word match (with case):
                if(x == y) {
                    score += 1;
                }

                // go case insensitive
                else {
                    x = x.toLowerCase();
                    y = y.toLowerCase();

                    // whole word match:
                    if(x == y) {
                        score += .8;
                    }

                    // shortened version (eg Rob for Robert or J for John):
                    else if(x.indexOf(y) == 0 || y.indexOf(x) == 0) {
                        score += .5;
                    }

                    // contained (eg Liz for Elizabeth):
                    else if(x.indexOf(y) >= 0 || y.indexOf(x) >= 0) {
                        score += .3;
                    }

                    // first letter (eg Jim for James):
                    else if(x.substr(0, 1) == y.substr(0, 1)) {
                        score += .1;
                    }
                }
            }
        }

        // max score will be 0.8. normalise to highest word count.
        return .8 * score / Math.max(p[0].length, p[1].length);
    },

    string: function(a, b) {

        if(a == b) {
            return 1;
        }

        a = a || '';
        b = b || '';

        var i, x, y;
        if(a.length > b.length) {
            x = a.toLowerCase().trim();
            y = b.toLowerCase().trim();
        } else {
            x = b.toLowerCase().trim();
            y = a.toLowerCase().trim();
        }

        if(x === '' || y === '') {
            return 0;
        }

        if(x.indexOf(y) != -1) {
            return .95 * y.length / x.length;
        }

        return 0;

    },

    amount: function(a, b) {

        if(a == b) {
            return 1;
        }

        var x = parseFloat((a || 0).toString().replace(/[^-.\d]+/g, '')),
            y = parseFloat((b || 0).toString().replace(/[^-.\d]+/g, ''));

        if(isNaN(x) || isNaN(y)) {
            return 0;
        }

        if(x == y) {
            return 1;
        }

        if(x == 0 || y == 0) {
            return 0;
        }

        return (x > y ? y / x : x / y) * 1.8 - 1;

    }
}

};

nf.UI.Pager.prototype = {

goTo: function(dest) {
    this.current = Math.max(Math.min(this.total, dest), 1);
    this.update();
    this.owner.update();
},

update: function(totalItems, itemsPerPage) {
    if(typeof totalItems == 'number' && typeof itemsPerPage == 'number') {
        this.total = Math.max(1, Math.ceil(totalItems / itemsPerPage));
        this.current = Math.max(Math.min(this.current, this.total), 1);
    }
    this.currentElement.innerHTML = this.current.toString();
    this.totalElement.innerHTML = this.total.toString();
    nf.className(this.rootElement, 'bof', this.current == 1 ? 1 : -1);
    nf.className(this.rootElement, 'eof', this.current == this.total ? 1 : -1);
},

init: function() {

    var l = nf.UI.Labels, pager = this;

    nf('<a>', this.rootElement, {
        className: 'first',
        href: 'javascript:',
        innerHTML: l.goToFirst.toHTML(),
        onclick: function() {
            pager.goTo(1)
        }
    });

    nf('<', this.rootElement, ' ');

    nf('<a>', this.rootElement, {
        className: 'previous',
        href: 'javascript:',
        innerHTML: l.goToPrevious.toHTML(),
        onclick: function() {
            pager.goTo(pager.current - 1)
        }
    });

    nf('<', this.rootElement, ' ');

    this.currentElement = nf('<span>', this.rootElement, {className: 'current'});

    nf('<', this.rootElement, ' ');

    nf('<span>', this.rootElement, {className: 'of', innerHTML: l.of.toHTML()});

    nf('<', this.rootElement, ' ');

    this.totalElement = nf('<span>', this.rootElement, {className: 'total'});

    nf('<', this.rootElement, ' ');

    nf('<a>', this.rootElement, {
        className: 'next',
        href: 'javascript:',
        innerHTML: l.goToNext.toHTML(),
        onclick: function() {
            pager.goTo(pager.current + 1)
        }
    });

    nf('<', this.rootElement, ' ');

    nf('<a>', this.rootElement, {
        className: 'last',
        href: 'javascript:',
        innerHTML: l.goToLast.toHTML(),
        onclick: function() {
            pager.goTo(pager.total)
        }
    });

    this.update();
}

};

nf.UI.Actions.prototype = {

add: function(id, label, callback) {
    if(!(id in this.items)) {
        this.items[id] = [label, callback];
        this.redraw();
    }
},
remove: function(id) {
    if(id in this.items) {
        delete this.items[id];
        this.redraw();
    }
},
redraw: function() {
    if(!this.rootElement) {
        return;
    }
    this.rootElement.innerHTML = '';
    var i = null;
    for(i in this.items) {
        nf('<a>', nf('<li>', this.rootElement, {className: i}), {
            href: 'javascript:',
            innerHTML: this.items[i][0].toHTML(),
            onclick: (function(c, that) {
                return function() {
                    c.call(that)
                }
            })(this.items[i][1], this.owner)
        });
    }
    if(i) {
        nf.className(this.rootElement.firstChild, 'first', 1);
        nf.className(this.rootElement.childNodes[this.rootElement.childNodes.length - 1], 'last', 1);
    }
    this.redrawSearch();
},
redrawSearch: function() {

    var searchValue = this.searchField ? this.searchField.value : '',
        id, li, l, o;

    if(this.owner.searchable) {

        l = nf.UI.Labels.search;

        li = nf('<li>', this.rootElement, {className: 'last search'});
        if(this.rootElement.childNodes.length == 1) {
            nf.className(li, 'first', 1);
        }
        else {
            nf.className(li.previousSibling, 'last', -1);
        }

        id = this.owner.appId + 'Search';

        nf('<label>', li, {
            innerHTML: l.toHTML(),
            htmlFor: id
        });

        o = this.owner;

        this.searchField = nf('<input>', li, {
            placeholder: l,
            title: l,
            type: 'text',
            id: id,
            value: searchValue,
            onkeyup: function() {
                o.update()
            }
        });

    }
}

};

// list view class nf.ListView = function(rootElement) {

this.rootElement = rootElement;
this.clear();

};

// list view class nf.ListView.prototype = {

addItem: function() {
    var ret = nf('<li>');
    this.rootElement.appendChild(ret);
    this.refreshClasses();
    return ret;
},
activateItem: function(item) {
    if(typeof item == 'number') {
        item = this.rootElement.childNodes[item];
    }
    var i = this.rootElement.childNodes.length;
    while(i--) {
        nf.className(
            this.rootElement.childNodes[i],
            'active',
                this.rootElement.childNodes[i] === item ? 1 : -1
        );
    }
},
removeItem: function(item) {
    if(typeof item == 'number') {
        item = this.rootElement.childNodes[item];
    }
    this.rootElement.removeChild(item);
    this.refreshClasses();
},
refreshClasses: function() {
    var c = this.rootElement.childNodes,
        i = c.length;
    while(i--) {
        nf.className(c[i], 'first', !i ? 1 : -1);
        nf.className(c[i], 'last', i == c.length - 1 ? 1 : -1);
    }
},
clear: function() {
    if(this.rootElement) {
        this.rootElement.innerHTML = '';
    }
}

};

nf.UI.Tests.fullName.salutations = “mr mrs ms dr prof”.split(' ');

nf.UI.Tests.email.variation = /.(com|net|org|gov|edu|{1,2})b.*|.+$/i;