define(["require", "exports", "utils", "item", "extendedStore", "combination", "filter"], function (require, exports, _utils, PhoenixItem, PhoenixExtendedStore, PhoenixCombination, PhoenixFilter) {
    // import PhoenixData = require("data");
    var PhoenixTable = (function () {
        // private wrapCombinationsAndMapToItemsAndStore(combinationsObj: any): PhoenixTable {
        // 	let validCombinationsObj = combinationsObj["valid"];
        // 	let invalidCombinationsObj = combinationsObj["invalid"];
        // 	$.each(["valid", "invalid"], (i, type) => {
        // 		let combinationArray = combinationsObj[type];
        // 		$.each(combinationArray, (i, combinationPairObj) => {
        // 			let a: Array<string> = combinationPairObj["a"];
        // 			let b: Array<string> = combinationPairObj["b"];
        // 			let aItems = $.map(a, (itemId, i) => {
        // 				return this.getItem(itemId);
        // 			});
        // 			let bItems = $.map(b, (itemId, i) => {
        // 				return this.getItem(itemId);
        // 			});
        // 			let combinationDescription = new PhoenixCombinationDescription(type, aItems, bItems);
        // 			this.addCombinationDescription(combinationDescription);
        // 		});	
        // 	});
        // 	return this;
        // }
        // This instance
        function PhoenixTable(id, data) {
            var _this = this;
            this.itemStore = new PhoenixExtendedStore();
            this.filterStore = new PhoenixExtendedStore();
            /** Memoised version of `.getData()`. */
            this.getData_m = _.memoize(this.getData);
            this.lookupItemCombinations_m = _.memoize(function (item) {
                var combinations = _this.getData_m("combinations");
                return _.reduce(combinations, function (filteredCombinations, typeContent, typeKey) {
                    filteredCombinations[typeKey] = _.filter(typeContent, { "a": [item] });
                    return filteredCombinations;
                }, {});
            });
            /**
             * Look up the combination arrays of an array of items (both `valid` and `invalid`.)
             *
             * This function is memoised.
             *
             * @param {Array<PhoenixItem>} The items to lookup.
             * @returns {*} The combination object.
             */
            this.lookupItemsCombinations_m = _.memoize(function (items) {
                var combinationObjArray = _.map(items, function (item) {
                    return _this.lookupItemCombinations_m(item);
                });
                // Get all "b" sides merged together
                var filteredCombinations = _.reduce(combinationObjArray, function (mergedCombinationObj, combinationObj) {
                    _.forEach(combinationObj, function (typeContent, typeKey) {
                        if (!mergedCombinationObj[typeKey]) {
                            mergedCombinationObj[typeKey] = [];
                        }
                        mergedCombinationObj[typeKey] = _.uniq(mergedCombinationObj[typeKey].concat(_.flatten(_.pluck(typeContent, "b"))));
                    });
                    return mergedCombinationObj;
                }, {});
                // TODO: Make each array unique?
                return filteredCombinations;
            });
            this.setId(id) // Set basic info
                .setData(data)
                .wrapItemsAndStore() // Create PhoenixItem objects
                .convertCombinationItemIdsToItems() // Convert combination item IDs to PhoenixItem objects
                .wrapFiltersAndStore() // Filter descriptions -> PhoenixFilter objects
            ;
            // // Map combinations into PhoenixCombination's
            // // this.wrapCombinationsAndMapToItemsAndStore(data["combinations"]);
            // // Strip `items` and `combinations` out
            // delete data["items"];
            // delete data["combinations"];
            // // Save remainder data as metadata
            // this.setMetadataObj(data);
            return this;
        }
        // private combinationDescriptions: Array<PhoenixCombinationDescription> = [];
        // private metadataStore: PhoenixExtendedStore<any> = new PhoenixExtendedStore<any>();
        // private filters: Array<PhoenixFilter>;
        // Name
        PhoenixTable.prototype.getId = function () {
            return this.id;
        };
        PhoenixTable.prototype.setId = function (id) {
            this.id = id;
            return this;
        };
        // Data
        PhoenixTable.prototype.getData = function (key) {
            if (key) {
                return this.getData()[key];
            }
            return this.data;
        };
        PhoenixTable.prototype.setData = function (data) {
            this.data = data;
            return this;
        };
        // Items
        // Items are OF ANY TYPE (e.g. "plan" or "extra")
        PhoenixTable.prototype.getItem = function (key) {
            return this.getItemStore().get(key);
        };
        PhoenixTable.prototype.getItems = function () {
            return this.getItemStore().convertValuesIntoArray();
        };
        PhoenixTable.prototype.getItemStore = function () {
            return this.itemStore;
        };
        PhoenixTable.prototype.getItemsOfType = function (type) {
            return _.filter(this.getItemStore().reveal(), function (item) {
                return item.getType() === type;
            });
        };
        PhoenixTable.prototype.getPlans = function () {
            return this.getItemsOfType("plan");
        };
        PhoenixTable.prototype.addItem = function (item) {
            this.getItemStore().set(item.getId(), item);
            return this;
        };
        // Filters
        PhoenixTable.prototype.getFilter = function (key) {
            return this.getFilterStore().get(key);
        };
        PhoenixTable.prototype.getFilters = function () {
            return this.getFilterStore().convertValuesIntoArray();
        };
        PhoenixTable.prototype.getFilterStore = function () {
            return this.filterStore;
        };
        PhoenixTable.prototype.addFilter = function (filter) {
            this.getFilterStore().set(filter.getId(), filter);
            return this;
        };
        // Combinations
        // getCombinationDescriptions(): Array<PhoenixCombinationDescription> {
        // 	return this.combinationDescriptions;
        // }
        // private addCombinationDescription(combination: PhoenixCombinationDescription): PhoenixTable {
        // 	this.combinationDescriptions.push(combination);
        // 	return this;
        // }
        // Metadata
        // getMetadataStore(): PhoenixExtendedStore<any> {
        // 	return this.metadataStore;
        // }
        // getMetadata(key: string): any {
        // 	return this.getMetadataStore().get(key);
        // }
        // private setMetadata(key: string, value: any): PhoenixTable {
        // 	this.getMetadataStore().set(key, value);
        // 	return this;
        // }
        // private setMetadataObj(obj: any): PhoenixTable {
        // 	this.getMetadataStore().extend(obj);
        // 	return this;
        // }
        // Special
        PhoenixTable.prototype.getTableColumns = function () {
            // return this.getMetadata("table")["columns"];
            return this.getData("table")["columns"];
        };
        PhoenixTable.prototype._wrapItemAndStore = function (itemData, itemId) {
            this.addItem(new PhoenixItem(itemId, itemData));
        };
        PhoenixTable.prototype.wrapItemsAndStore = function () {
            _.forEach(this.getData("items"), this._wrapItemAndStore, this);
            return this;
        };
        PhoenixTable.prototype._wrapFilterAndStore = function (filterData, filterId) {
            this.addFilter(new PhoenixFilter(filterId, filterData));
        };
        PhoenixTable.prototype.wrapFiltersAndStore = function () {
            _.forEach(this.getData("filters"), this._wrapFilterAndStore, this);
            return this;
        };
        /**
         * Look up the combination arrays of an array of items (both `valid` and `invalid`.)
         *
         * This function is memoised internally.
         *
         * @param {Array<PhoenixItem>} The items to lookup.
         * @returns {*} The combination object.
         */
        PhoenixTable.prototype.lookupItemsCombinations = function (items) {
            return this.lookupItemsCombinations_m(_.sortBy(_.uniq(items)));
        };
        ;
        PhoenixTable.prototype.convertCombinationItemIdsToItems = function () {
            var _this = this;
            // ... for each "type"
            _.forEach(this.getData("combinations"), function (arrayOfCombinations) {
                // ... for each combination
                _.forEach(arrayOfCombinations, function (combinationObj) {
                    // ... for each array
                    _.forEach(combinationObj, function (arrayOfItemIds) {
                        // ... for each item ID
                        _.forEach(arrayOfItemIds, function (itemId, index) {
                            // Convert ID to PhoenixItem
                            arrayOfItemIds[index] = _this.getItem(itemId);
                        });
                    });
                });
            });
            return this;
        };
        PhoenixTable.prototype.createQuery = function () {
            return new PhoenixTableQuery(this);
        };
        return PhoenixTable;
    })();
    var PhoenixTableQuery = (function () {
        function PhoenixTableQuery(table) {
            this.queryParameters = [];
            this.valid = false;
            this.accumulator = {
                infQueries: 0,
                ceilQueries: 0,
                floorQueries: 0
            };
            return this
                .setTable(table);
        }
        PhoenixTableQuery.prototype.getTable = function () {
            return this.table;
        };
        PhoenixTableQuery.prototype.setTable = function (table) {
            this.table = table;
            return this;
        };
        PhoenixTableQuery.prototype.getQueryParameters = function () {
            return this.queryParameters;
        };
        /**
         * Remember `value` must be normalised!
         */
        PhoenixTableQuery.prototype.addQueryParameter = function (filter, type, value) {
            var queryParamDesc = {
                filter: filter,
                type: type,
                value: value
            };
            // Validation
            switch (type) {
                // // Infinity query type: maximise this parameter's value
                // case "inf":
                // 	// Verify that we have a numeric value
                // 	if (filter.getBaseType() != "number") {
                // 		throw new Error(`Query parameter type "inf" can only apply to filter types which are numeric.`);
                // 	}
                // 	// Accumulate so we know how many can result in unbounded searches
                // 	++this.accumulator.infQueries;
                // 	break;
                // Ceiling query type: this parameter's value is not to exceed `value`.
                case "ceil":
                    // Verify that we have a numeric value
                    if (typeof value != "number") {
                        throw new Error("Query parameter type \"ceil\" expects value of type number.");
                    }
                    if (filter.getBaseType() != "number") {
                        throw new Error("Query parameter type \"ceil\" can only apply to filter types which are numeric.");
                    }
                    // Accumulate so we know how many are "capping" the query
                    ++this.accumulator.ceilQueries;
                    break;
                // Floor query type: this parameter's value is not to fall below `value`.
                case "floor":
                    // Verify that we have a numeric value
                    if (typeof value != "number") {
                        throw new Error("Query parameter type \"floor\" expects value of type number.");
                    }
                    if (filter.getBaseType() != "number") {
                        throw new Error("Query parameter type \"floor\" can only apply to filter types which are numeric.");
                    }
                    // Accumulate so we know how many are "capping" the query
                    ++this.accumulator.floorQueries;
                    break;
                // Match query type: parameter value must match EXACTLY to `value`.
                case "match":
                    break;
                // Contains query type: parameter value must contain `value`.
                case "contains":
                    break;
                default:
                    throw new Error("Query parameter type \"" + type + "\" not recognised.");
            }
            // Once partially checked, push into the array
            this.queryParameters.push(queryParamDesc);
            return this;
        };
        PhoenixTableQuery.prototype.isValid = function () {
            return this.valid;
        };
        PhoenixTableQuery.prototype.setValid = function () {
            this.valid = true;
            return this;
        };
        PhoenixTableQuery.prototype.validate = function () {
            if (this.accumulator.floorQueries > 0 && this.accumulator.ceilQueries === 0) {
                throw new Error("Queries must contain at least one parameter that prevents unbounded searches.");
            }
            return this
                .setValid();
        };
        PhoenixTableQuery.availableExtensionItems = function (combinationSet, table) {
            var items;
            if (combinationSet instanceof PhoenixCombination) {
                items = combinationSet.getItems();
            }
            else {
                items = combinationSet;
            }
            return this.availableExtensionItems_m(items.sort(_utils.sorterNumeric), table);
        };
        // Very slow for some reason
        PhoenixTableQuery.prototype.runFilterOnCombination = function (combination, secondRoundComputation) {
            var valid = true;
            var representativeItem = combination.getRepresentativeItem();
            var representativeData = representativeItem.getData();
            var queryParameters = this.getQueryParameters();
            for (var _i = 0; _i < queryParameters.length; _i++) {
                var _a = queryParameters[_i], filter = _a.filter, type = _a.type, value = _a.value;
                // if (type === "inf") {
                // 	continue;	// We don't really care about the validity of "inf" dimensions at present
                // }
                var itemValue = _utils.returnField(representativeData, filter.getField());
                switch (type) {
                    // Ceiling query type: this parameter's value is not to exceed `value`.
                    case "ceil":
                        // if (secondRoundComputation){ break; }
                        if (itemValue > value) {
                            valid = false;
                            break;
                        }
                        break;
                    // Floor query type: this parameter's value is not to fall below `value`.
                    // Can only be applied as a second round computation.
                    case "floor":
                        if (!secondRoundComputation) {
                            break;
                        }
                        if (itemValue === -1) {
                            break;
                        }
                        // Implict itemValue != -1 from above
                        if (value === -1) {
                            valid = false;
                            break;
                        }
                        if (itemValue < value) {
                            valid = false;
                            break;
                        }
                        break;
                    // Match query type: parameter value must match EXACTLY to `value`.
                    case "match":
                        // if (secondRoundComputation){ break; }
                        break;
                    // Contains query type: parameter value must contain `value`.
                    case "contains":
                        // if (secondRoundComputation){ break; }
                        break;
                    default:
                        throw new Error("Query parameter type \"" + type + "\" not recognised.");
                }
                if (!valid) {
                    break;
                }
            }
            return valid;
        };
        PhoenixTableQuery.prototype.execute = function () {
            if (!this.isValid()) {
                throw new Error("This query has not been validated.");
            }
            // Start
            var startTime = performance.now();
            // let maxOutputLength = 10000;
            // let maxCycleTime = 1000;	// in ms
            var basePlans = this.getTable().getPlans();
            // Need to run:
            // 1. get plans and add to temp set
            // 2. filter temp set, push to `output` not necessary
            // 3. pass to loop (for loop on `output`)
            // 4. -- loop for each in `output`:
            // 5.    get extension items and add to temp set
            // 6.    filter temp set, push to `output`
            // 7. -- continue loop until exhausted or stop condition met
            // 0.
            var combinationsArray;
            var combinationsPreviousItemsArray = [];
            // 1.
            combinationsArray = _.map(basePlans, function (basePlanItem) {
                return new PhoenixCombination([basePlanItem]);
            });
            // We only continue with combinations if the query is not blank.
            if (this.queryParameters.length > 0) {
                var currComb;
                var currCombItems;
                var newAvailItems;
                var newCombItems;
                var newCombItemString;
                var newCombObj;
                var newCombItemArrayLength = 0;
                // 3., 4.
                for (var i = 0; i <= combinationsArray.length; ++i) {
                    // let cycleStartTime = performance.now();
                    // --- previous 
                    // Discard any older ones if they do not meet criteria in second round (e.g. "floor")
                    // let discardOldCombination: boolean = false;
                    if (i > 0 && !this.runFilterOnCombination(combinationsArray[i - 1], true)) {
                        // Remove by shifting i down and splicing at that point
                        // i needs to be shifted otherwise the remainder of the loop will be misaligned
                        --i;
                        combinationsArray.splice(i, 1);
                    }
                    // We've run the last cycle
                    if (i >= combinationsArray.length) {
                        break;
                    }
                    // --- current 
                    currComb = combinationsArray[i];
                    currCombItems = _.clone(currComb.getItems());
                    // 5.
                    newAvailItems = PhoenixTableQuery.availableExtensionItems(currComb, this.getTable());
                    for (var _i = 0; _i < newAvailItems.length; _i++) {
                        var newItem = newAvailItems[_i];
                        // Create the array of items to generate into a combination
                        newCombItems = _.clone(currCombItems);
                        newCombItems.push(newItem);
                        // Convert to string for check
                        newCombItemString = newCombItems.sort(_utils.sorterNumeric).toString();
                        // Check if length has incremented
                        // We can do this emptying of the array since we append n+1 length combinations to the end of the array, so the length monotonically increases
                        if (newCombItemArrayLength < newCombItems.length) {
                            combinationsPreviousItemsArray = []; // Empty array since it is now useless for checking
                            newCombItemArrayLength = newCombItems.length;
                        }
                        // Skip combinations that have already been considered
                        if (_.indexOf(combinationsPreviousItemsArray, newCombItemString) > -1) {
                            continue;
                        }
                        // 6.
                        newCombObj = new PhoenixCombination(newCombItems);
                        if (!this.runFilterOnCombination(newCombObj)) {
                            continue;
                        }
                        combinationsArray.push(newCombObj);
                        combinationsPreviousItemsArray.push(newCombItemString);
                    }
                }
            }
            var endTime = performance.now();
            return {
                combinations: combinationsArray,
                time: (endTime - startTime)
            };
        };
        PhoenixTableQuery.availableExtensionItems_m = _.memoize(function (items, table) {
            var _a = table.lookupItemsCombinations(items), valid = _a.valid, invalid = _a.invalid;
            // Remove self-references in `invalid` (can't kill yourself)
            // May not be necessary?
            // _.remove(invalidItems, item => item === basePlanItem);
            // Filter out illegal combinations
            var availableItems = _.difference(valid, invalid);
            // Filter out non-stackable items if they have already been used
            availableItems = _.difference(availableItems, _.filter(items, function (item) {
                return (item.getData("stackable") === false);
            }));
            return availableItems;
        });
        return PhoenixTableQuery;
    })();
    return PhoenixTable;
});
