/**
 * A minimal library for pivoting data by 1-n dimensions.
 * 
 * The {@link pivot} function slices and dices data by one or more {@link Dimension dimensions}, returning a {@link Matrix} if one {@link Dimension} is passed, a {@link Cube} if two
 * {@link Dimension dimensions} are passed, and a {@link Hypercube} if more than two {@link Dimension dimensions} are passed.
 * 
 * Simple {@link Dimension dimensions} can be created by mapping a set of values using the {@link property} function and a property name from the data set to be pivoted.
 * 
 * Once a {@link Cube} is created, the {@link query} function can be used to perform query operations on the subset of the source data in each cell.
 * 
 * @module
 */

/**
 * A simple function, taking an agrument and returning a result.
 * @typeParam TArg The type of the argument passed into the function.
 * @typeParam TResult The type of the result provided by the functions.
 * @typeParam arg The argument passed into the function.
 * @category Type declarations
 */
export type Function<TArg, TResult> = (arg: TArg) => TResult;

/**
 * A predicate is a boolean function, used as point on a {@link Dimension} used to evaluate source data for a specific condition.
 * @typeParam TValue The type of the source data that the predicate was created for.
 * @category Type declarations
 */
export type Predicate<TValue> = Function<TValue, boolean>;

/**
 * A dimension is a set of {@link Predicate} used to partition data.
 * @typeParam TValue The type of the source data that the {@link Dimension} was created for.
 * @category Type declarations
 */
export type Dimension<TValue> = Array<Predicate<TValue>>;

/**
 * A matrix is a two dimensional data structure.
 * @typeParam TValue The type of the source data that the matrix was created from.
 * @category Type declarations
 */
export type Matrix<TValue> = Array<Array<TValue>>;

/**
 * A cube is a three dimensional data structure.
 * @typeParam TValue The type of the source data that the cube was created from.
 * @category Type declarations
 */
export type Cube<TValue> = Matrix<Array<TValue>>;

/**
 * An n-cube is an n-dimensional data structure.
 * @category Type declarations
 */
export type Hypercube = Cube<Array<any>>;

/**
 * Creates a {@link Dimension} from some source data that will be used to slice and dice.
 * @typeParam TCriteria The type of the seed data used to creat the dimension.
 * @param criteria The seed data for the dimension; one entry in the source array will be one point on the dimension.
 * @param generator A function that creates a {@link Predicate} for each point on the dimension.
 * The following code creates a {@link Dimension} that will be used to evaluate ```Player``` objects during a {@link pivot} operation based on the value of their ```position``` property:
 * ```ts
 * const positions: string[] = ['Goalkeeper', 'Defender', 'Midfielder', 'Forward'];
 * const x = dimension(positions, property<Player>('position'));
 * ```
 * See {@link https://github.com/steelbreeze/pivot/blob/main/src/example/index.ts GitHub} for a complete example.
 * @category Cube building
 */
export const dimension = <TCriteria, TValue>(criteria: Array<TCriteria>, generator: Function<TCriteria, Predicate<TValue>>): Dimension<TValue> =>
	map(criteria, generator);

/**
 * Creates a predicate function {@link Predicate} for use in the {@link dimension} function to create a {@link Dimension} matching properties.
 * @typeParam TValue The type of the source data that will be evaluated by the generated predicate.
 * @param key The property in the source data to base this {@link Predicate} on.
 * @example
 * The following code creates a {@link Dimension} that will be used to evaluate ```Player``` objects during a {@link pivot} operation based on the value of their ```position``` property:
 * ```ts
 * const positions: string[] = ['Goalkeeper', 'Defender', 'Midfielder', 'Forward'];
 * const x = dimension(positions, property<Player>('position'));
 * ```
 * See {@link https://github.com/steelbreeze/pivot/blob/main/src/example/index.ts GitHub} for a complete example.
 * @category Cube building
 */
export const property = <TValue>(key: keyof TValue): Function<TValue[keyof TValue], Predicate<TValue>> =>
	(criterion: TValue[keyof TValue]) => (value: TValue) => value[key] === criterion;

/**
 * Slices data by one dimension, returning a {@link Matrix}.
 * @typeParam TValue The type of the source data to be sliced.
 * @param values The source data, an array of objects.
 * @param dimension The dimension to slice the data by.
 * @example
 * The following code creates a {@link Cube}, slicing and dicing the squad data for a football team by player position and country:
 * ```ts
 * const y = dimension(countries, (country: string) => (player: Player) => player.country === country); // using a user-defined generator
 * 
 * const cube: Matrix<Player> = slice(squad, y);
 * ```
 * @category Cube building
 * @remarks This is equivalent to {@link pivot} with one dimension.
 */
export const slice = <TValue>(values: Array<TValue>, dimension: Dimension<TValue>): Matrix<TValue> =>
	map(dimension, (predicate: Predicate<TValue>) => filter(values, predicate));

/**
 * Slices and dices source data by one or more dimensions, returning, {@link Matrix}, {@link Cube} or {@link Hypercube} depending on the number of dimensions passed.
 * See the overloads for more detail.
 * @example
 * The following code creates a {@link Cube}, slicing and dicing the squad data for a football team by player position and country:
 * ```ts
 * const x = dimension(positions, property<Player>('position')); // using the built-in dimension generator matching a property
 * const y = dimension(countries, (country: string) => (player: Player) => player.country === country); // using a user-defined generator
 * 
 * const cube: Cube<Player> = pivot(squad, y, x);
 * ```
 * @category Cube building
 */
export const pivot: {
	/**
	 * Slices data by two dimensions, returning a {@link Cube}.
	 * @typeParam TValue The type of the source data to be sliced and diced.
	 * @param source The source data, an array of objects.
	 * @param first The first dimension to slice the data by.
	 */
	<TValue>(source: Array<TValue>, first: Dimension<TValue>): Matrix<TValue>;

	/**
	 * Slices data by two dimensions, returning a {@link Cube}.
	 * @typeParam TValue The type of the source data to be sliced and diced.
	 * @param source The source data, an array of objects.
	 * @param first The first dimension to slice the data by.
	 * @param second The second dimension to dice the data by.
	 */
	<TValue>(source: Array<TValue>, first: Dimension<TValue>, second: Dimension<TValue>): Cube<TValue>;

	/**
	 * Slices data by three or more dimensions, returning a {@link Hypercube}.
	 * @typeParam TValue The type of the source data to be sliced and diced.
	 * @param source The source data, an array of objects.
	 * @param first The first dimension to slice the data by.
	 * @param others Two or more other dimensions to pivot the data by.
	 */
	<TValue>(source: Array<TValue>, first: Dimension<TValue>, ...others: Array<Dimension<TValue>>): Hypercube;
} = <TValue>(values: Array<TValue>, first: Dimension<TValue>, ...[second, ...others]: Array<Dimension<TValue>>) =>
		second ? map(slice(values, first), (sliced: Array<TValue>) => pivot(sliced, second, ...others)) : slice(values, first);

/**
 * Queries data from a {@link Matrix} using a selector {@link Function} to transform the objects in each cell of data in the {@link Matrix} into a result.
 * @typeParam TValue The type of the data within the {@link Matrix}.
 * @typeParam TResult The type of value returned by the selector.
 * @param matrix The {@link Matrix} to query data from.
 * @param selector A callback {@link Function} to create a result from each cell of the {@link Cube}.
 * @remarks The {@link Matrix} may also be a {@link Cube} or {@link Hypercube}.
 * @example
 * The following code queries a {@link Cube}, returning the {@link average} age of players in a squad by country by position:
 * ```ts
 * const x = dimension(positions, property<Player>('position')); // using the built-in dimension generator matching a property
 * const y = dimension(countries, (country: string) => (player: Player) => player.country === country); // using a user-defined generator
 * 
 * const cube: Cube<Player> = pivot(squad, y, x);
 * 
 * const result: Matrix<number> = query(cube, average(age()));
 * 
 * function age(asAt: Date = new Date()): Function<Player, number> {
 *   return player => new Date(asAt.getTime() - player.dateOfBirth.getTime()).getUTCFullYear() - 1970;
 * }
 * ```
 * See {@link https://github.com/steelbreeze/pivot/blob/main/src/example/index.ts GitHub} for a complete example.
 * @category Cube query
 */
export const query = <TValue, TResult>(matrix: Matrix<TValue>, selector: Function<TValue, TResult>): Matrix<TResult> =>
	map(matrix, (sliced: Array<TValue>) => map(sliced, selector));

/**
 * Create a callback {@link Function} to pass into {@link query} that sums numerical values derived by the selector {@link Function}.
 * @typeParam TValue The type of the data within the cube that will be passed into the selector.
 * @param selector A callback {@link Function} to derive a numerical value for each object in the source data.
 * @example
 * The following code queries a {@link Cube}, returning the {@link average} age of players in a squad by country by position:
 * ```ts
 * const x = dimension(positions, property<Player>('position')); // using the built-in dimension generator matching a property
 * const y = dimension(countries, (country: string) => (player: Player) => player.country === country); // using a user-defined generator
 * 
 * const cube: Cube<Player> = pivot(squad, y, x);
 * 
 * const result: Matrix<number> = query(cube, sum(age()));
 * 
 * function age(asAt: Date = new Date()): Function<Player, number> {
 *   return player => new Date(asAt.getTime() - player.dateOfBirth.getTime()).getUTCFullYear() - 1970;
 * }
 * ```
 * See {@link https://github.com/steelbreeze/pivot/blob/main/src/example/index.ts GitHub} for a complete example.
 * @category Cube query
 */
export const sum = <TValue>(selector: Function<TValue, number>): Function<Array<TValue>, number> =>
	(values: Array<TValue>) => reduce(values, (accumulator: number, value: TValue) => accumulator + selector(value), 0);

/**
 * Create a callback {@link Function} to pass into {@link query} that averages numerical values derived by the selector {@link Function}.
 * @typeParam TValue The type of the data within the cube that will be passed into the selector.
 * @param selector A callback {@link Function} to derive a numerical value for each object in the source data.
 * @example
 * The following code queries a {@link Cube}, returning the {@link average} age of players in a squad by country by position:
 * ```ts
 * const x = dimension(positions, property<Player>('position')); // using the built-in dimension generator matching a property
 * const y = dimension(countries, (country: string) => (player: Player) => player.country === country); // using a user-defined generator
 * 
 * const cube: Cube<Player> = pivot(squad, y, x);
 * 
 * const result: Matrix<number> = query(cube, average(age()));
 * 
 * function age(asAt: Date = new Date()): Function<Player, number> {
 *   return player => new Date(asAt.getTime() - player.dateOfBirth.getTime()).getUTCFullYear() - 1970;
 * }
 * ```
 * See {@link https://github.com/steelbreeze/pivot/blob/main/src/example/index.ts GitHub} for a complete example.
 * @category Cube query
 */
export const average = <TValue>(selector: Function<TValue, number>): Function<Array<TValue>, number> =>
	(values: Array<TValue>) => sum(selector)(values) / values.length;

// fast alternative to Array.prototype.filter
function filter<TValue>(values: Array<TValue>, predicate: Predicate<TValue>): Array<TValue> {
	const result: Array<TValue> = [];

	for (let index = 0; index < values.length; ++index) {
		if (predicate(values[index])) {
			result.push(values[index]);
		}
	}

	return result;
}

// fast alternative to Array.prototype.map
function map<TValue, TResult>(values: Array<TValue>, mapper: Function<TValue, TResult>): Array<TResult> {
	const result: Array<TResult> = [];

	for (let index = 0; index < values.length; ++index) {
		result.push(mapper(values[index]));
	}

	return result;
}

// fast alternative to Array.prototype.reduce
function reduce<TValue, TResult>(values: Array<TValue>, reducer: (accumulator: TResult, value: TValue) => TResult, initialValue: TResult): TResult {
	let accumulator: TResult = initialValue;

	for (let index = 0; index < values.length; ++index) {
		accumulator = reducer(accumulator, values[index]);
	}

	return accumulator;
}
