
import { Functions } from './Functions';
import { Objects } from "./Objects";
import { Types } from "./Types";

export type SortableType = string | number | Date | boolean;

export class Sorter<T> {

    public static readonly COMPARATORS = {
        get number() {
            return (a: number, b: number) => a - b;
        },
        get string() {
            return (a: string, b: string, stringCompareOptions ?: Intl.CollatorOptions) => a.localeCompare(b, undefined, stringCompareOptions);
        },
        get boolean() {
            return (a: boolean, b: boolean) => {
                if (!a && b) {
                    return 1;
                } else if (a && !b) {
                    return -1;
                } else {
                    return 0;
                }
            };
        },
        get Date() {
            return (a: Date, b: Date) => {
                return a.getTime() - b.getTime();
            };
        },
        get null() {
            return (a: SortableType, b: SortableType) => {
                if (!Objects.isNotNullOrUndefined(a) && Objects.isNotNullOrUndefined(b)) {
                    return 1;
                } else if (Objects.isNotNullOrUndefined(a) && !Objects.isNotNullOrUndefined) {
                    return -1;
                } else {
                    return 0;
                }
            };
        }
    };

    private _nullsLast : boolean = true;
    private _stringCollatorOptions ?: Intl.CollatorOptions;
    private _inverse : boolean = false;
    private readonly extractor : Functions.MapFunction<T, T[keyof T] & SortableType>;

    public static byField<T>(fieldname : keyof T & Types.KeysWithValsOfType<T, SortableType>) : Sorter<T> {
        return new Sorter<T>(fieldname);
    }

    public static byExtractor<T>(extractor : Functions.MapFunction<T, T[keyof T] & SortableType>) : Sorter<T> {
        return new Sorter(extractor);
    }   

    private constructor(extractorOrFieldname : Functions.MapFunction<T, T[keyof T] & SortableType> | (keyof T & Types.KeysWithValsOfType<T, SortableType>)) {
        if (Objects.isFunction(extractorOrFieldname)) {
            this.extractor = extractorOrFieldname;
        } else {
            this.extractor = Functions.extractor(extractorOrFieldname) as Functions.MapFunction<T, T[keyof T] & SortableType>;
        }
    }

    public nullsLast(value : boolean) : this {
        this._nullsLast = value;
        return this;
    }

    public inverse() : this {
        this._inverse = !this._inverse;
        return this;
    }

    public stringCollatorOptions(value : Intl.CollatorOptions) : this {
        this._stringCollatorOptions = value;
        return this;
    }

    public build(nextSort ?: Functions.Comparator<T>) : Functions.Comparator<T> {
        return (a: T, b: T) : number => {
            const avalue : SortableType =  this.extractor(a);
            const bvalue : SortableType =  this.extractor(b);
            const result = this.compareValues(avalue, bvalue);
            if (result === 0 && Objects.isNotNullOrUndefined(nextSort)) {
                return nextSort(a, b);
            }
            return result;
        };
    }

    private compareValues(avalue: SortableType, bvalue: SortableType) : number {
        const nullCheck : number = this._nullsLast ? Sorter.COMPARATORS.null(avalue, bvalue) : -1 * Sorter.COMPARATORS.null(avalue, bvalue);
        if (nullCheck !== 0) {
            return nullCheck;
        }

        let result : number;
        if (Objects.isString(avalue) && Objects.isString(bvalue)) {
            result = Sorter.COMPARATORS.string(avalue, bvalue, this._stringCollatorOptions);
        } else if (Objects.isNumeric(avalue) && Objects.isNumeric(bvalue)) {
            result = Sorter.COMPARATORS.number(avalue, bvalue);
        } else if (Objects.isBoolean(avalue) && Objects.isBoolean(bvalue)) {
            result = Sorter.COMPARATORS.boolean(avalue, bvalue);
        } else if (avalue instanceof Date && bvalue instanceof Date) {
            result = Sorter.COMPARATORS.Date(avalue, bvalue);
        } else if (!Objects.isNotNullOrUndefined && !Objects.isNotNullOrUndefined(bvalue)) {
            result = nullCheck;
        } else {
            throw new Error("Not supported combination of types");
        }

        if (this._inverse) {
            result = -1 * result;
        }
        return result;
    }
}
