// Knockout Observable Dictionary
// (c) James Foster
// License: MIT (http://www.opensource.org/licenses/mit-license.php)

(function () {
    function DictionaryItem(key, value, dictionary) {
        var observableKey = new ko.observable(key);

        this.value = new ko.observable(value);
        this.key = new ko.computed({
            read: observableKey,
            write: function (newKey) {
                var current = observableKey();

                if (current == newKey) return;

                // no two items are allowed to share the same key.
                dictionary.remove(newKey);

                observableKey(newKey);
            }
        });
    }

    ko.observableDictionary = function (dictionary, keySelector, valueSelector) {
        var result = {};

        result.items = new ko.observableArray();

        result._wrappers = {};
        result._keySelector = keySelector || function (value, key) { return key; };
        result._valueSelector = valueSelector || function (value) { return value; };

        if (typeof keySelector == 'string') result._keySelector = function (value) { return value[keySelector]; };
        if (typeof valueSelector == 'string') result._valueSelector = function (value) { return value[valueSelector]; };

        ko.utils.extend(result, ko.observableDictionary['fn']);

        result.pushAll(dictionary);

        return result;
    };

    ko.observableDictionary['fn'] = {
        remove: function (valueOrPredicate) {
            var predicate = valueOrPredicate;

            if (valueOrPredicate instanceof DictionaryItem) {
                predicate = function (item) {
                    return item.key() === valueOrPredicate.key();
                };
            }
            else if (typeof valueOrPredicate != "function") {
                predicate = function (item) {
                    return item.key() === valueOrPredicate;
                };
            }

            ko.observableArray['fn'].remove.call(this.items, predicate);
        },

        push: function (key, value) {
            var item = null;

            if (key instanceof DictionaryItem) {
                // handle the case where only a DictionaryItem is passed in
                item = key;
                value = key.value();
                key = key.key();
            }

            if (value === undefined) {
                value = this._valueSelector(key);
                key = this._keySelector(value);
            }
            else {
                value = this._valueSelector(value);
            }

            var current = this.get(key, false);
            if (current) {
                // update existing value
                current(value);
                return current;
            }

            if (!item) {
                item = new DictionaryItem(key, value, this);
            }

            ko.observableArray['fn'].push.call(this.items, item);

            return value;
        },

        pushAll: function (dictionary) {
            var self = this;
            var items = self.items();

            if (dictionary instanceof Array) {
                $.each(dictionary, function (index, item) {
                    var key = self._keySelector(item, index);
                    var value = self._valueSelector(item);
                    items.push(new DictionaryItem(key, value, self));
                });
            }
            else {
                for (var prop in dictionary) {
                    if (dictionary.hasOwnProperty(prop)) {
                        var item = dictionary[prop];
                        var key = self._keySelector(item, prop);
                        var value = self._valueSelector(item);
                        items.push(new DictionaryItem(key, value, self));
                    }
                }
            }

            self.items.valueHasMutated();
        },

        sort: function (method) {
            if (method === undefined) {
                method = function (a, b) {
                    return defaultComparison(a.key(), b.key());
                };
            }

            return ko.observableArray['fn'].sort.call(this.items, method);
        },

        indexOf: function (key) {
            if (key instanceof DictionaryItem) {
                return ko.observableArray['fn'].indexOf.call(this.items, key);
            }

            var underlyingArray = this.items();
            for (var index = 0; index < underlyingArray.length; index++) {
                if (underlyingArray[index].key() == key)
                    return index;
            }
            return -1;
        },

        get: function (key, wrap) {
            if (wrap == false)
                return getValue(key, this.items());

            var wrapper = this._wrappers[key];

            if (wrapper == null) {
                wrapper = this._wrappers[key] = new ko.computed({
                    read: function () {
                        var value = getValue(key, this.items());
                        return value ? value() : null;
                    },
                    write: function (newValue) {
                        var value = getValue(key, this.items());

                        if (value)
                            value(newValue);
                        else
                            this.push(key, newValue);
                    }
                }, this);
            }

            return wrapper;
        },

        set: function (key, value) {
            return this.push(key, value);
        },

        keys: function () {
            return ko.utils.arrayMap(this.items(), function (item) { return item.key(); });
        },

        values: function () {
            return ko.utils.arrayMap(this.items(), function (item) { return item.value(); });
        },
        
        removeAll: function () {
            this.items.removeAll();
        },

        toJSON: function () {
            var result = {};
            var items = ko.utils.unwrapObservable(this.items);

            ko.utils.arrayForEach(items, function (item) {
                var key = ko.utils.unwrapObservable(item.key);
                var value = ko.utils.unwrapObservable(item.value);

                result[key] = value;
            });

            return result;
        }
    };

    function getValue(key, items) {
        var found = ko.utils.arrayFirst(items, function (item) {
            return item.key() == key;
        });
        return found ? found.value : null;
    }
})();


// Utility methods
// ---------------------------------------------
function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

function defaultComparison(a, b) {
    if (isNumeric(a) && isNumeric(b)) return a - b;

    a = a.toString();
    b = b.toString();

    return a == b ? 0 : (a < b ? -1 : 1);
}
// ---------------------------------------------
