"use strict";
var redux_1 = require("redux");
/**
 * Defines an action, for later inclusion in a reducer.
 */
function action(type, reduce) {
    function create(payload) {
        return { type: type, payload: payload };
    }
    return assign(create, {
        type: type,
        reduce: reduce,
        payloadType: undefined,
        stateType: undefined
    });
}
exports.action = action;
function isAction(obj, type) {
    return obj && obj.type === type;
}
function chain(headType, empty, head, rest) {
    function reduce(state, action) {
        if (isAction(action, headType)) {
            return head(state, action.payload);
        }
        return rest(state, action);
    }
    return assign(reduce, {
        actionType: undefined,
        cursorType: undefined,
        empty: empty,
        action: function (def) {
            return chain(def.type, empty, def.reduce, reduce);
        },
        store: function () {
            return redux_1.createStore(reduce);
        }
    });
}
/**
 * Creates a starter object from which a Reducer can be formed by
 * calling the action method.
 */
function reducer(empty) {
    return {
        action: function (def) {
            return chain(def.type, empty, def.reduce, function (s) { return s === undefined ? empty : s; });
        }
    };
}
exports.reducer = reducer;
function cursor(state, dispatch) {
    var outer;
    function navigate(ref, key) {
        return ref(outer, key);
    }
    outer = assign(dispatch, {
        $: navigate,
        state: state,
        valueOf: function () {
            return state;
        }
    });
    return outer;
}
exports.cursor = cursor;
/**
 * Takes a snapshot of a Redux-like store, making it into a pure cursor.
 */
function snapshot(store) {
    return cursor(store.getState(), function (action) {
        store.dispatch(action);
        return snapshot(store);
    });
}
exports.snapshot = snapshot;
/**
 * Creates a function capable of making cursors, given an outer
 * cursor (a whole store or large portion of a store) and a key
 * (such as a string or number) that identifies a smaller portion
 * of the store.
 *
 * Internally this consists of a function for fetching the inner
 * state and a function for creating an action to update the outer
 * state.
 */
function items(fetch, update) {
    return function (outer, key) {
        return cursor(fetch(outer.state, key), function (innerAction) {
            return items(fetch, update)(outer(update(key, innerAction)), key);
        });
    };
}
exports.items = items;
/**
 * Creates a function capable of making cursors, given an outer
 * cursor, but no key (a singular version of the items function.)
 */
function item(fetch, update) {
    var impl = items(fetch, function (_, action) { return update(action); });
    return function (outer) { return impl(outer, undefined); };
}
exports.item = item;
function immutableMapOperations() {
    return {
        get: function (items, key, emptyItem) {
            return items.has(key) ? items.get(key) : emptyItem;
        },
        set: function (items, key, item) {
            return items.set(key, item);
        },
        remove: function (items, key, emptyItem) {
            return items.remove(key);
        }
    };
}
exports.immutableMapOperations = immutableMapOperations;
var builtInOperations = {
    get: function (items, key, emptyItem) {
        return Object.prototype.hasOwnProperty.call(items, key) ? items[key] : emptyItem;
    },
    set: function (items, key, item) {
        return amend(items, (_a = {}, _a[key] = item, _a));
        var _a;
    },
    remove: function (items, key) {
        items = assign({}, items);
        delete items[key];
        return items;
    }
};
function stringMapOperations() {
    return builtInOperations;
}
exports.stringMapOperations = stringMapOperations;
function numberMapOperations() {
    return builtInOperations;
}
exports.numberMapOperations = numberMapOperations;
function arrayOperations() {
    return {
        get: function (items, key, emptyItem) {
            var r = items[key];
            return r === undefined ? emptyItem : r;
        },
        set: function (items, key, item) {
            items = items.slice(0);
            items.splice(key, 1, item);
            return items;
        },
        remove: function (items, key, emptyItem) {
            items = items.slice(0);
            items[key] = emptyItem;
            return items;
        }
    };
}
exports.arrayOperations = arrayOperations;
function collection(optionsOrType, opt_reducer, opt_operations, opt_get, set) {
    var type;
    var operations;
    var reducer;
    var get;
    if (typeof optionsOrType === "string") {
        type = optionsOrType;
        operations = opt_operations;
        reducer = opt_reducer;
        get = opt_get;
    }
    else {
        type = optionsOrType.type;
        operations = optionsOrType.operations;
        reducer = optionsOrType.reducer;
        get = optionsOrType.get;
        set = optionsOrType.set;
    }
    function add(key) {
        return { type: type, payload: { key: key } };
    }
    function remove(key) {
        return { type: type, payload: { key: key, remove: true } };
    }
    function update(key, update) {
        return { type: type, payload: { key: key, update: update } };
    }
    var ensuredSet = ensureReducer("collection(" + type + ")", get, set);
    function reduce(state, _a) {
        var key = _a.key, update = _a.update, remove = _a.remove;
        var collection = get(state);
        var value = operations.get(collection, key, reducer.empty);
        return ensuredSet(state, remove
            ? operations.remove(collection, key, reducer.empty)
            : operations.set(collection, key, update
                ? reducer(value, update)
                : value));
    }
    var plainCursors = items(function (state, key) { return operations.get(get(state), key, reducer.empty); }, update);
    var collectionCursors = function (outer, key) {
        var plainCursor = plainCursors(outer, key);
        return assign(plainCursor, {
            remove: function () { return outer(remove(key)); }
        });
    };
    return assign(collectionCursors, {
        type: type,
        reduce: reduce,
        update: update,
        add: add,
        remove: remove,
        payloadType: undefined,
        stateType: undefined
    });
}
exports.collection = collection;
function reference(optionsOrType, opt_reducer, opt_get, set) {
    var type;
    var reducer;
    var get;
    if (typeof optionsOrType === "string") {
        type = optionsOrType;
        reducer = opt_reducer;
        get = opt_get;
    }
    else {
        type = optionsOrType.type;
        reducer = optionsOrType.reducer;
        get = optionsOrType.get;
        set = optionsOrType.set;
    }
    var ensuredSet = ensureReducer("reference(" + type + ")", get, set);
    function update(payload) {
        return { type: type, payload: payload };
    }
    function reduce(outerState, innerAction) {
        return ensuredSet(outerState, reducer(get(outerState), innerAction));
    }
    return assign(item(function (state) { return get(state); }, update), {
        type: type,
        reduce: reduce,
        update: update,
        payloadType: undefined,
        stateType: undefined
    });
}
exports.reference = reference;
exports.REPLACE = "REPLACE";
/**
 * Defines the reducer for a value that can only be assigned a whole new value.
 * It only supports the action "REPLACE" whose payload is the replacement value.
 */
function primitive() {
    return reducer(undefined).action(action(exports.REPLACE, function (s, v) { return v; }));
}
exports.primitive = primitive;
/**
 * Action that replaces a whole value, supported by primitives
 */
function replace(value) {
    return { type: exports.REPLACE, payload: value };
}
exports.replace = replace;
/**
 * Defines a property, which is a simple value that can be
 * replaced with a new value. It uses the primitive reducer.
 */
function property(type, fetch, reduce) {
    return reference(type, primitive(), fetch, reduce);
}
exports.property = property;
function assign(target) {
    var sources = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        sources[_i - 1] = arguments[_i];
    }
    for (var _a = 0, sources_1 = sources; _a < sources_1.length; _a++) {
        var source = sources_1[_a];
        for (var _b = 0, _c = Object.keys(source); _b < _c.length; _b++) {
            var key = _c[_b];
            target[key] = source[key];
        }
    }
    return target;
}
exports.assign = assign;
/**
 * Pretty good subsitute for object spread syntax. Instead of:
 *
 *    { ...book, title }
 *
 * say:
 *
 *    amend(book, { title })
 */
function amend(o1, o2) {
    return assign({}, o1, o2);
}
exports.amend = amend;
// Oh yes, I went there...
var matchFunction = /function\s*[a-z]*\s*\(\s*([a-z]+)\s*\)\s*\{\s*return\s+([a-z]+)\.([a-z]+)/i;
var matchLambda = /\(?\s*([a-z]+)\s*\)?\s*\=\>\s*([a-z]+)\.([a-z]+)/i;
function ensureReducer(context, fetch, reduce) {
    if (reduce) {
        return reduce;
    }
    // We might be able to generate reduce by parsing the source of fetch!
    var src = fetch.toString();
    matchFunction.lastIndex = 0;
    matchLambda.lastIndex = 0;
    var matched = matchFunction.exec(src) || matchLambda.exec(src);
    if (!matched) {
        throw new Error("Cannot generate reducer for " + context + " "
            + "- too complex to parse, needs explicit reduce");
    }
    if (matched[1] !== matched[2]) {
        throw new Error("Cannot generate reducer for " + context + " " +
            ("- inconsistent parameter usage: " + matched[1] + ", " + matched[2]));
    }
    return function (state, value) {
        return amend(state, (_a = {}, _a[matched[3]] = value, _a));
        var _a;
    };
}
//# sourceMappingURL=index.js.map