import is from '@mezzy/is';
import {
    ICompareFunction,
    IEqualsFunction,
    ILoopFunction
} from '@mezzy/function-types';
import ArrayTools from './arrayTools';
import IList from './interfaces/iList';


export class List<T> implements IList<T> {
    constructor(items?: T[]) {
        this.p_array = [];

        if (is.notEmpty(items)) {
            for (let i:number = 0; i < items.length; i++) { this.add(items[i]); }
        }
    }


    /*====================================================================*
     START: Static
     *====================================================================*/
    static fromArray<T>(array:T[]):List<T> {
        let list:List<T> = new List<T>();
        for (let i:number = 0; i < array.length; i++) { list.add(array[i]); }
        return list;
    }


    /*====================================================================*
     START: Properties
     *====================================================================*/
    get isEmpty():boolean { return this.p_array.length <= 0; }
    get size():number { return this.p_array.length; }
    get first():T { return this.p_array[0]; }
    get last():T { return this.p_array[this.p_array.length]; }


    get array():Array<T> { return this.p_array; }
    protected p_array:Array<T>;


    item(index:number):T {
        let item:T = this.p_array[index];
        return is.notEmpty(item) ? item : null;
    }


    /*====================================================================*
     START: Methods
     *====================================================================*/
    indexOf(item:T, fromIndex:number = 0):number { return this.p_array.indexOf(item, fromIndex); }


    add(item:T, index?:number):void {
        if (is.empty(index)) index = this.p_array.length;
        if (index < 0 || index > this.p_array.length || is.empty(item)) return;
        this.p_array.splice(index, 0, item);
    }


    /**
     * Appends all the items in the provided list to this list.
     */
    append(list:List<T>):void {
        if (is.empty(list)) return;

        list.forEach((item:T) => {
            if (is.notEmpty) {
                this.add(item);
                return true;
            } else return false;
        });
    }


    copy():List<T> {
        let list:List<T> = new List<any>();
        for (let index = 0; index < this.p_array.length; index++) {
            list.add(this.p_array[index])
        }
        return list;
    }

    forEach(callback:ILoopFunction<T>):void {
        this.p_array.forEach(callback);
    }

    sort(compareFunction:ICompareFunction<T>):void {
        this.p_array.sort(compareFunction);
    }

    /**
     * Replaces/updates an element in this list.
     * Returns true if the element was replaced or false if the index is invalid or if the element is undefined.
     */
    replace(item:T, index:number):void { this.p_array.splice(index, 1, item); }


    /**
     * Returns the index in this list of the first occurrence of the
     * specified element, or -1 if the List does not contain this element.
     * <p>If the elements inside this list are
     * not comparable with the === operator a custom equals function should be
     * provided to perform searches, the function must receive two arguments and
     * return true if they are equal, false otherwise. Example:</p>
     *
     * <pre>
     * let petsAreEqualByName = function(pet1, pet2) {
     *     return pet1.key === pet2.key;
     * }
     * </pre>
     */
    search(item:T, equalsFunction?:IEqualsFunction<T>):number {
        if (is.empty(equalsFunction)) equalsFunction = (a:T, b:T):boolean => { return a === b; };
        if (is.empty(item)) return -1;

        for (let index = 0; index < this.p_array.length; index++) {
            if (equalsFunction(this.p_array[index], item)) return index;
        }

        return -1;
    }


    /**
     * Returns true if this list has the specified element.
     * <p>If the elements inside the list are
     * not comparable with the === operator a custom equals function should be
     * provided to perform searches, the function must receive two arguments and
     * return true if they are equal, false otherwise. Example:</p>
     *
     * <pre>
     * let petsAreEqualByName = function(pet1, pet2) {
    *     return pet1.key === pet2.key;
    * }
     * </pre>
     */
    has(item:T):boolean { return (this.p_array.indexOf(item) >= 0); }
    // has(item:T, equalsFunction?:IEqualsFunction<T>):boolean {
    //     return (this.indexOf(item, equalsFunction) >= 0);
    // }


    /**
     * Removes the first occurrence of the specified element in this list.
     * <p>If the elements inside the list are
     * not comparable with the === operator a custom equals function should be
     * provided to perform searches, the function must receive two arguments and
     * return true if they are equal, false otherwise. Example:</p>
     *
     * <pre>
     * let petsAreEqualByName = (pet1, pet2) => {
     *     return pet1.key === pet2.key;
     * }
     * </pre>
     */
    delete(item:T, equalsFunction?:IEqualsFunction<T>):boolean {
        if (is.empty(equalsFunction)) equalsFunction = (a:T, b:T):boolean => { return a === b; };
        if (this.p_array.length < 1 || is.empty(item)) return false;

        for (let index = 0; index < this.p_array.length; index++) {
            if (equalsFunction(this.p_array[index], item)) {
                this.p_array.splice(index, 1);
                return true;
            }
        }

        return false;
    }


    deleteAtIndex(index:number):T {
        if (index < 0 || index >= this.p_array.length) return undefined;
        let item:T = this.p_array[index];
        this.p_array.splice(index, 1);
        return item;
    }


    clear():void { this.p_array.splice(0, this.p_array.length); }


    /**
     * Returns true if this list is equal to the given list.
     * Two lists are equal if they have the same elements in the same order.
     * @param {List} other the other list.
     * @param {function(Object,Object):boolean=} equalsFunction optional
     * function used to check if two elements are equal. If the elements in the lists
     * are custom objects you should provide a function, otherwise
     * the === operator is used to check equality between elements.
     * @return {boolean} true if this list is equal to the given list.
     */
    equals(other:List<T>, equalsFunction?:IEqualsFunction<T>):boolean {
        if (is.empty(equalsFunction)) equalsFunction = (a:T, b:T):boolean => { return a === b; };
        if (!(other instanceof List)) return false;
        if (this.size !== other.size) return false;

        for (let index = 0; index < this.p_array.length; index++) {
            if (!equalsFunction(this.p_array[index], other.array[index])) return false;
        }

        return true;
    }


    toString():string { return ArrayTools.toString(this.copy().array); }
} // End class


export default List;
