import {
  Type,
  PrimitiveType,
  PropertyKeyType,
} from '../type';


export class TypeUtil {
  /**
   * Get the provide constructor.
   * @param target
   */
  public static getContructor<T>(target: any): Type<T> {
    return typeof target === 'function' ? target : target.constructor;
  }

  /**
   * Get the provide constructor if target is an instance.
   * @param target
   * @returns {*}
   */
  public static getType<T>(target: any): Type<T> {
    return target.prototype ? target : target.constructor;
  }

  /**
   * Get the provide prototype if target is an instance.
   * @param target
   * @returns {*}
   */
  public static getPrototype(target: any): Object {
    return typeof target === 'function' ? target.prototype : target;
  }

  /**
   *
   * @param target
   * @returns {symbol}
   */
  public static getTypeOrSymbol(target: any): any {
      return typeof target === 'symbol' ? target : TypeUtil.getType(target);
  }

  /**
   * Return true if the given obj is a primitive.
   * @param target
   * @returns {boolean}
   */
  public static isPrimitiveOrPrimitiveType(target: any): boolean {
    return TypeUtil.isString(target)
        || TypeUtil.isNumber(target)
        || TypeUtil.isBoolean(target);
  }

  /**
   *
   * @param target
   * @returns {PrimitiveType}
   */
  public static primitiveOf(target: any): PrimitiveType {
    if (TypeUtil.isString(target)) {
        return PrimitiveType.STRING;
    }
    if (TypeUtil.isNumber(target)) {
        return PrimitiveType.NUMBER;
    }
    if (TypeUtil.isBoolean(target)) {
        return PrimitiveType.BOOLEAN;
    }
    return PrimitiveType.ANY;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isString(target: any): boolean {
    return typeof target === 'string' || target instanceof String || target === String;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isNumber(target: any): boolean {
    return typeof target === 'number' || target instanceof Number || target === Number;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isBoolean(target: any): boolean {
    return typeof target === 'boolean' || target instanceof Boolean || target === Boolean;
  }

  /**
   *
   * @param target
   * @returns {Boolean}
   */
  public static isArray(target: any): boolean {
    return Array.isArray(target);
  }

  /**
   * Return true if the clazz is an array.
   * @param target
   * @returns {boolean}
   */
  public static isArrayOrArrayType(target: any): boolean {
    if (target === Array) {
        return true;
    }
    return TypeUtil.isArray(target);
  }

  /**
   * Return true if the target.
   * @param target
   * @returns {boolean}
   */
  public static isCollection(target: any): boolean {
    return TypeUtil.isArrayOrArrayType(target)
        || target === Map
        || target instanceof Map
        || target === Set
        || target instanceof Set
        || target === WeakMap
        || target instanceof WeakMap
        || target === WeakSet
        || target instanceof WeakSet;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isDate(target: any): boolean {
    return target === Date || target instanceof Date;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isMethod(target: any, propertyKey: PropertyKeyType): boolean {
    if (typeof(target[propertyKey]) === undefined) {
      return false;
    }
    return typeof target[propertyKey] === 'function';
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isObject(target: any): boolean {
    return target === Object;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isType(target: any): boolean {
    return !TypeUtil.isPrimitiveOrPrimitiveType(target)
        && !TypeUtil.isObject(target)
        && !TypeUtil.isDate(target)
        && target !== undefined
        && !TypeUtil.isPromise(target);
  }

  /**
   * Return true if the value is an empty string, null or undefined.
   * @param value
   * @returns {boolean}
   */
  public static isEmpty(value: any): boolean {
    return value === '' || value === null || value === undefined;
  }

  /**
   * Get object name
   */
  public static nameOf(obj: any): string {
    switch (typeof obj) {
        default:
            return '' + obj;
        case 'symbol':
            return TypeUtil.nameOfSymbol(obj);
        case 'function':
            return TypeUtil.nameOfType(obj);
    }
  }

  /**
   * Get the provide name.
   * @param target
   */
  public static nameOfType(target: any): string {
    return typeof target === 'function'
        ? target.name
        : target.constructor.name;
  }
  /**
   * Get symbol name.
   * @param sym
   */
  public static nameOfSymbol(sym: symbol): string {
    return sym.toString().replace('Symbol(', '').replace(')', '');
  }
  /**
   *
   * @param out
   * @param obj
   * @param {{[p: string]: (collection: any[], value: any) => any}} reducers
   * @returns {any}
   */
  public static deepExtends(out: any, obj: any, reducers: { [key: string]: (collection: any[], value: any) => any } = {}): any {

    if (obj === undefined || obj === null) {
        return obj;
    }

    if (TypeUtil.isPrimitiveOrPrimitiveType(obj) || typeof obj === 'symbol' || typeof obj === 'function') {
        return obj;
    }

    if (TypeUtil.isArrayOrArrayType(obj)) {
        out = out || [];
    } else {
        out = out || {};
    }

    const defaultReducer = reducers.default ? reducers.default : (collection: any[], value: any) => {
        collection.push(value);
        return collection;
    };
    const set = (key: string | number, value: any) => {
        if (TypeUtil.isArrayOrArrayType(obj)) {
            out.push(value);
        } else {
            out[key] = value;
        }
    };

    Object.keys(obj).forEach(key => {
        let value = obj[key];

        if (value === undefined || value === null) {
            return;
        }

        if (value === '' && out[key] !== '') {
            return;
        }

        if (TypeUtil.isPrimitiveOrPrimitiveType(value) || typeof value === 'function') {
            set(key, value);
            return;
        }

        if (TypeUtil.isArrayOrArrayType(value)) {

            value = value.map((v: any) => TypeUtil.deepExtends(undefined, v));

            set(key, []
                .concat(out[key] || [], value)
                .reduce((collection: any[], v: any) =>
                        reducers[key] ? reducers[key](collection, v) : defaultReducer(collection, v),
                        []));
            return;
        }

        // Object
        if (TypeUtil.isArrayOrArrayType(obj)) {
            set(key, TypeUtil.deepExtends(undefined, value, reducers));
        } else {
            set(key, TypeUtil.deepExtends(out[key], value, reducers));
        }
    });

    if (TypeUtil.isArrayOrArrayType(out)) {
        out.reduce((collection: any[], value: any) => defaultReducer(collection, value), []);
    }

    return out;
  }

  /**
   *
   * @param target
   * @returns {boolean}
   */
  public static isPromise(target: any): boolean {
    return target === Promise || target instanceof Promise;
  }

  /**
   *
   * @param target
   * @returns {any}
   */
  public static getInheritedType(target: Type<any>): Type<any> {
    return Object.getPrototypeOf(target);
  }

  /**
   *
   * @param target
   * @param {PropertyKeyType} propertyKey
   * @returns {PropertyDescriptor}
   */
  public static descriptorOf(target: any, propertyKey: PropertyKeyType): PropertyDescriptor {
    return Object.getOwnPropertyDescriptor(target && target.prototype || target, propertyKey);
  }

  /**
   *
   * @param target
   * @param {PropertyKeyType} propertyKey
   * @returns {string[]}
   */
  public static getParameterNames(target: any, propertyKey: PropertyKeyType): string[] {
    const rawType = TypeUtil.getPrototype(target);
    const fn: Function = rawType[propertyKey];

    const code = fn.toString()
                .replace(COMMENTS, '')
                .replace(FAT_ARROWS, '')
                .replace(DEFAULT_PARAMS, '');

    const result = code.slice(code.indexOf('(') + 1, code.indexOf(')')).match(/([^\s,]+)/g);

    return result === null
    ? []
    : result;
  }

  /**
   *
   * @param target
   * @returns {Array}
   */
  public static ancestorsOf(target: Type<any>): Type<any>[] {
    const classes: Type<any>[] = [];

    let currentTarget = TypeUtil.getType(target);

    while (TypeUtil.nameOf(currentTarget) !== '') {
        classes.unshift(currentTarget);
        currentTarget = TypeUtil.getInheritedType(currentTarget);
    }

    return classes;
  }

  /**
   *
   * @param target
   * @param {string} name
   * @param {Function} callback
   */
  public static applyBefore(target: any, name: string, callback: Function): void {
    const original = target[name];
    target[name] = function (...args: any[]): any {
        callback(...args);
        return original.apply(this, args);
    };
  }

  /**
   *
   * @param {Promise<any>} promise
   * @param {number} time
   * @returns {Promise<any>}
   */
  public static promiseTimeout(promise: Promise<any>, time: number = 1000): Promise<{ ok: boolean, response: any }> {
    const timeout = (p: Promise<any>, t: number) => new Promise((resolve) => {
        p.then((response) => {
            resolve();
            return response;
        });
        setTimeout(() => resolve({ok: false}), t);
    });

    promise = promise.then((response) => ({ok: true, response}));

    return Promise.race([
        promise,
        timeout(promise, time),
    ]);
  }

}

const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
const DEFAULT_PARAMS = /=[^,]+/mg;
const FAT_ARROWS = /=>.*$/mg;
