import { isPresent, isNumeric, isDate } from './utils';
import { getter } from './accessor';
import type { Combinator, Predicate, Reducer, Transformer, TransformerResult, Preprocessors } from './common.interfaces';

const valueToString = (value): string => {
    value = isPresent(value) && value.getTime ? value.getTime() : value;
    return value + "";
};

/**
 * @hidden
 */
export const groupCombinator = <T>(field, preprocessors: Preprocessors<T> = {}) => {
    const prop = a => {
        const value = getter(field, true)(a);
        const preprocessor = preprocessors[field];
        return preprocessor ? preprocessor(value) : value;
    };

    let position = 0;

    return (agg, value) => {
        agg[field] = agg[field] || {};

        const groupValue = prop(value);
        const key = valueToString(groupValue);
        const values = agg[field][key] || { __position: position++, aggregates: {}, items: [], value: groupValue };

        values.items.push(value);

        agg[field][key] = values;

        return agg;
    };
};

// tslint:disable:max-line-length
/**
 * The result of the [aggregate operation]({% slug api_kendo-data-query_aggregateby %}) where the keys are the field name used for the calculation and the values are an object with the aggregate function name and a resulting value.
 *
 * @example
 * ```js-no-run
 * const result = <AggregateResult>{
 *      "unitPrice": { "sum": 53, "count": 2 },
 *      "unitsInStock": { "sum": 66, "count": 3 }
 * };
 *
 * console.log(result["unitPrice"].sum); //53
 *
 * ```
 */
// tslint:enable:max-line-length
export interface AggregateResult {
    [fieldName: string]: {
        count?: number;
        sum?: number;
        average?: number;
        min?: number;
        max?: number;
        count_distinct?: number
    };
}

/**
 * @hidden
 */
export const expandAggregates = (result = {}): AggregateResult => {
    Object.keys(result).forEach(field => {
        const aggregates = result[field];
        Object.keys(aggregates).forEach(aggregate => {
            aggregates[aggregate] = aggregates[aggregate].result();
        });
    });
    return result;
};

/*
 * this function checks if value is applicable to count_distinct in accordance with Reporting BE logic
 * https://servicetitan.atlassian.net/browse/RD-15293?focusedCommentId=2337000
 * null, undefined, empty string, and strings consisting of only whitespaces are not counted
 * 0 values are counted
 * all other values are counted
 */
const isDistinctValue = (value: any) => {
    if(value === null || value === undefined) {
        return false;
    }
    if(typeof value === "string" && !value.trim().length) {
        return false;
    }
    return true;
}

const aggregatesFuncs = (name) => ({
    average: () => {
        let value = 0;
        let count = 0;
        let lastNonNumeric: any = undefined;
        return {
            calc: (curr) => {
                if (isNumeric(curr)) {
                    value += curr;
                    count++;
                } else {
                    lastNonNumeric = curr;
                }
            },
            result: () => count ? value / count : lastNonNumeric
        };
    },
    count: () => {
        let state = 0;
        return {
            calc: () => state++,
            result: () => state
        };
    },
    max: () => {
        let state = Number.NEGATIVE_INFINITY;
        return {
            calc: (value) => {
                state = isNumeric(state) || isDate(state) ? state : value;

                if (state < value && (isNumeric(value) || isDate(value))) {
                    state = value;
                }
            },
            result: () => state
        };
    },
    min: () => {
        let state = Number.POSITIVE_INFINITY;
        return {
            calc: (value) => {
                state = isNumeric(state) || isDate(state) ? state : value;

                if (state > value && (isNumeric(value) || isDate(value))) {
                    state = value;
                }
            },
            result: () => state
        };
    },
    sum: () => {
        let state = 0;
        return {
            calc: (value) => state += value,
            result: () => state
        };
    },
    count_distinct: () => {
        const state = new Set();
        return {
            calc: (value) => isDistinctValue(value) && state.add(
                 typeof value === 'object' && value.toString
                    ? value.toString()
                    : value
            ),
            result: () => state.size,
        };
    }
}[name]());

/**
 * @hidden
 */
export const aggregatesCombinator = <T>(descriptors, preprocessors: Preprocessors<T> = {}) => {
    const functions = descriptors.map(descriptor => {
        const fieldAccessor = a => {
            const value = getter(descriptor.field, true)(a);
            const preprocessor = preprocessors[descriptor.field];
            return preprocessor ? preprocessor(value) : value;
        };
        const aggregateName = (descriptor.aggregate || "").toLowerCase();
        const aggregateAccessor = getter(aggregateName, true);

        return (state, value) => {
            const fieldAggregates = state[descriptor.field] || {};

            const aggregateFunction = aggregateAccessor(fieldAggregates)
                || aggregatesFuncs(aggregateName);

            aggregateFunction.calc(fieldAccessor(value));

            fieldAggregates[descriptor.aggregate] = aggregateFunction;

            state[descriptor.field] = fieldAggregates;

            return state;
        };
    });

    return (state, value) => functions.reduce((agg, calc) => calc(agg, value), state);
};

/**
 * @hidden
 * Adds the value to the `arr` and produces a new array.
 *
 * > The original array will be modified.
 */
export const concat = <T>(arr: T[], value: T): T[] => {
    arr.push(value);
    return arr;
};

/**
 * @hidden
 * Returns a reducer that will apply the specified transformation to the value.
 */
export const map = (transform): Reducer => (
    (reduce: Combinator): Transformer => (
        (acc, curr, index) => reduce(acc, transform(curr, index))
    )
);

/**
 * @hidden
 * Returns a reducer that will filter out items which do not match the `Predicate`.
 */
export const filter = (predicate): Reducer => (
    (reduce: Combinator): Transformer => (
        (acc, curr) => predicate(curr) ? reduce(acc, curr) : acc
    )
);

/**
 * @hidden
 */
export const isTransformerResult = <T>(source: TransformerResult<T>): source is TransformerResult<T> => {
    return isPresent((<TransformerResult<T>>source).__value);
};

const reduced = <T>(x): TransformerResult<T> => {
    if (isTransformerResult(x)) {
        return <TransformerResult<T>>x;
    }
    return {
        __value: x,
        reduced: true
    };
};

/**
 * @hidden
 * Returns a reducer that will take the specified number of items.
 */
export const take = (count: number): Reducer => (
    (reduce: Combinator): Transformer => (
        (acc, curr) => count-- > 0 ? reduce(acc, curr) : reduced(acc)
    )
);

/**
 * @hidden
 * Returns a reducer that will take the specified number of items.
 */
export const takeWhile = (predicate: Predicate): Reducer => (
    (reduce: Combinator): Transformer => (
        (acc, curr) => predicate(curr) ? reduce(acc, curr) : reduced(acc)
    )
);

/**
 * @hidden
 * Returns a reducer that will skip the specified number of items.
 */
export const skip = (count: number): Reducer => (
    (reduce: Combinator): Transformer => (
        (acc, curr) => count-- <= 0 ? reduce(acc, curr) : acc
    )
);

/**
 * @hidden
 * Transforms the data by applying the supplied transformer.
 */
export const exec = <T, U>(transform: Transformer, initialValue: U, data: T[]): U => {
    let result: any = initialValue;

    for (let idx = 0, length = data.length; idx < length; idx++) {
        result = transform(result, data[idx], idx);

        if (isTransformerResult(result)) {
            result = result.__value;
            break;
        }
    }

    return result;
};
