/**
 * Helper Utility for objects and instances
 */
export class ObjectExtensions {
    /**
     * Check whether an object is null or undefined
     * @param object The object to check.
     */
    public static isNullOrUndefined(object: any): boolean {
        if (!!!object) return true; // This doesn't always work
        if (object === null) return true;
        if (object === undefined) return true;
        return false;
    }
    /**
     * Get all methods attached to an object
     * @param object The object to check.
     */
    public static getAllMethods<TClass extends object>(object: TClass): Array<Function> {
        if (this.isNullOrUndefined(object)) throw new ReferenceError('Cannot get methods from an undefined object');

        // Credit where credit is due: http://stackoverflow.com/a/35033472/2319865

        let props = [];
        do {
            const methodList = Object.getOwnPropertyNames(object)
                .concat((<any>Object).getOwnPropertySymbols(object).map(s => s.toString()))
                .sort()
                .filter((p, i, arr) =>
                    typeof object[p] === 'function' &&      // Only the methods
                    p !== 'constructor' &&                  // Not the constructor
                    (i === 0 || p !== arr[i - 1]) &&        // Not overriding in this prototype
                    props.indexOf(p) === -1                 // Not overridden in a child
                )
                .map(methodName => object[methodName]);
            props = props.concat(methodList);
            object = Object.getPrototypeOf(object);         // Walk-up the prototype chain
        }
        while (
            !ObjectExtensions.isNullOrUndefined(object) &&
            // Not the the Object prototype methods (hasOwnProperty, etc...)
            !ObjectExtensions.isNullOrUndefined(Object.getPrototypeOf(object))
        );

        return props;
    }

    /* Todo: Don't know how to test this */
    /**
     * Get the propertyDecorator for this object
     * @param object The object to check.
     * @param propertyName The name of the property the decorator is attached to.
     * @param descriptorName The name of the descript value where the decorator is attached to.
     */
    public static getPropertyDecorator<TDecorator extends Object>(
        object: Object,
        propertyName: string,
        descriptorName: string): TDecorator {

        if (!object.hasOwnProperty(propertyName)) return null;
        let descriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(object, propertyName);
        if (ObjectExtensions.isNullOrUndefined(descriptor) ||
            ObjectExtensions.isNullOrUndefined(descriptor.value)) return null;

        return descriptor.value[descriptorName];
    }
}

/* istanbul ignore start */

/**
 * Interface to allow extension code completion
 */
export interface Object {
    /**
     * Check whether an object is null or undefined
     */
    isNullOrUndefined(): boolean;
    /**
     * Get all methods attached to an object
     */
    getAllMethods<TClass extends Object>(): Array<Function>;
    /**
     * Get the propertyDecorator for this object
     * @param propertyName The name of the property the decorator is attached to.
     * @param descriptorName The name of the descript value where the decorator is attached to.
     */
     getPropertyDecorator<TDecorator extends Object>(
        propertyName: string,
        descriptorName: string): TDecorator;
}

export default ObjectExtensions;

/* istanbul ignore next */
/**
 * Apply extensions to the Object interface
 */
// tslint:disable-next-line:no-unused-expression
!function applyObjectExtensions(): void
{
    Object.prototype['isNullOrUndefined'] = () => ObjectExtensions.isNullOrUndefined(this);
    Object.prototype['getAllMethods'] = () => ObjectExtensions.getAllMethods(this);
    Object.prototype['getPropertyDecorator'] = (propertyName, descriptorName) =>
        ObjectExtensions.getPropertyDecorator(this, propertyName, descriptorName);
}();

/* istanbul ignore end */