import isPlainObject from 'lodash/isPlainObject';
import d from 'debug';
import global from './global';

export { global };

/**
 * 调试日志控制器
 * @const
 * @memberof module:leancloud-realtime
 * @example
 * debug.enable();  // 启用调试日志
 * debug.disable(); // 关闭调试日志
 */
const debug = {
  enable: (namespaces = 'LC*') => d.enable(namespaces),
  disable: d.disable,
};
export { debug };

export const tryAll = promiseConstructors => {
  const promise = new Promise(promiseConstructors[0]);
  if (promiseConstructors.length === 1) {
    return promise;
  }
  return promise.catch(() => tryAll(promiseConstructors.slice(1)));
};

// eslint-disable-next-line no-sequences
export const tap = interceptor => value => (interceptor(value), value);

export const finalize = callback => [
  // eslint-disable-next-line no-sequences
  value => (callback(), value),
  error => {
    callback();
    throw error;
  },
];

export { default as Expirable } from './expirable';
export { default as Cache } from './cache';

/**
 * 将对象转换为 Date,支持 string、number、ProtoBuf Long 以及 LeanCloud 的 Date 类型,
 * 其他情况下(包括对象为 falsy)返回原值。
 * @private
 */
export const decodeDate = date => {
  if (!date) return date;
  if (typeof date === 'string' || typeof date === 'number') {
    return new Date(date);
  }
  if (date.__type === 'Date' && date.iso) {
    return new Date(date.iso);
  }
  // Long
  if (typeof date.toNumber === 'function') {
    return new Date(date.toNumber());
  }
  return date;
};
/**
 * 获取 Date 的毫秒数,如果不是一个 Date 返回 undefined。
 * @private
 */
export const getTime = date =>
  date && date.getTime ? date.getTime() : undefined;

/**
 * 解码对象中的 LeanCloud 数据结构。
 * 目前仅会处理 Date 类型。
 * @private
 */
export const decode = value => {
  if (!value) return value;
  if (value.__type === 'Date' && value.iso) {
    return new Date(value.iso);
  }
  if (Array.isArray(value)) {
    return value.map(decode);
  }
  if (isPlainObject(value)) {
    return Object.keys(value).reduce(
      (result, key) => ({
        ...result,
        [key]: decode(value[key]),
      }),
      {}
    );
  }
  return value;
};
/**
 * 将对象中的特殊类型编码为 LeanCloud 数据结构。
 * 目前仅会处理 Date 类型。
 * @private
 */
export const encode = value => {
  if (value instanceof Date) return { __type: 'Date', iso: value.toJSON() };
  if (Array.isArray(value)) {
    return value.map(encode);
  }
  if (isPlainObject(value)) {
    return Object.keys(value).reduce(
      (result, key) => ({
        ...result,
        [key]: encode(value[key]),
      }),
      {}
    );
  }
  return value;
};

export const keyRemap = (keymap, obj) =>
  Object.keys(obj).reduce((newObj, key) => {
    const newKey = keymap[key] || key;
    return Object.assign(newObj, {
      [newKey]: obj[key],
    });
  }, {});

export const isIE10 =
  global.navigator &&
  global.navigator.userAgent &&
  global.navigator.userAgent.indexOf('MSIE 10.') !== -1;

/* eslint-disable no-proto */
export const getStaticProperty = (klass, property) =>
  klass[property] ||
  (klass.__proto__ ? getStaticProperty(klass.__proto__, property) : undefined);
/* eslint-enable no-proto */

export const union = (a, b) => Array.from(new Set([...a, ...b]));
export const difference = (a, b) =>
  Array.from((bSet => new Set(a.filter(x => !bSet.has(x))))(new Set(b)));

const map = new WeakMap();

// protected property helper
export const internal = object => {
  if (!map.has(object)) {
    map.set(object, {});
  }
  return map.get(object);
};

export const compact = (obj, filter) => {
  if (!isPlainObject(obj)) return obj;
  const object = { ...obj };
  Object.keys(object).forEach(prop => {
    const value = object[prop];
    if (value === filter) {
      delete object[prop];
    } else {
      object[prop] = compact(value, filter);
    }
  });
  return object;
};

// debug utility
const removeNull = obj => compact(obj, null);
export const trim = message => removeNull(JSON.parse(JSON.stringify(message)));

export const ensureArray = target => {
  if (Array.isArray(target)) {
    return target;
  }
  if (target === undefined || target === null) {
    return [];
  }
  return [target];
};

export const setValue = (target, key, value) => {
  // '.' is not allowed in Class keys, escaping is not in concern now.
  const segs = key.split('.');
  const lastSeg = segs.pop();
  let currentTarget = target;
  segs.forEach(seg => {
    if (currentTarget[seg] === undefined) currentTarget[seg] = {};
    currentTarget = currentTarget[seg];
  });
  currentTarget[lastSeg] = value;
  return target;
};

export const isWeapp =
  // eslint-disable-next-line no-undef
  typeof wx === 'object' && typeof wx.connectSocket === 'function';

// throttle decorator
export const throttle = wait => (target, property, descriptor) => {
  const callback = descriptor.value;
  // very naive, internal use only
  if (callback.length) {
    throw new Error('throttled function should not accept any arguments');
  }
  return {
    ...descriptor,
    value() {
      let { throttleMeta } = internal(this);
      if (!throttleMeta) {
        throttleMeta = {};
        internal(this).throttleMeta = throttleMeta;
      }
      let { [property]: propertyMeta } = throttleMeta;
      if (!propertyMeta) {
        propertyMeta = {};
        throttleMeta[property] = propertyMeta;
      }
      const { previouseTimestamp = 0, timeout } = propertyMeta;
      const now = Date.now();
      const remainingTime = wait - (now - previouseTimestamp);
      if (remainingTime <= 0) {
        throttleMeta[property].previouseTimestamp = now;
        callback.apply(this);
      } else if (!timeout) {
        propertyMeta.timeout = setTimeout(() => {
          propertyMeta.previouseTimestamp = Date.now();
          delete propertyMeta.timeout;
          callback.apply(this);
        }, remainingTime);
      }
    },
  };
};

export const isCNApp = appId => appId.slice(-9) !== '-MdYXbMMI';

export const equalBuffer = (buffer1, buffer2) => {
  if (!buffer1 || !buffer2) return false;
  if (buffer1.byteLength !== buffer2.byteLength) return false;
  const a = new Uint8Array(buffer1);
  const b = new Uint8Array(buffer2);
  return !a.some((value, index) => value !== b[index]);
};