import Vue from 'vue';

function parseData (data, vueComponentOptions) {
  if (!data) {
    return
  }
  vueComponentOptions.mpOptions.data = data;
}

function parseComponents (vueComponentOptions) {
  vueComponentOptions.components = global.__wxVueOptions.components;
}

const _toString = Object.prototype.toString;
const hasOwnProperty = Object.prototype.hasOwnProperty;

function isFn (fn) {
  return typeof fn === 'function'
}

function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}

function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

function noop () {}

/**
 * Create a cached version of a pure function.
 */
function cached (fn) {
  const cache = Object.create(null);
  return function cachedFn (str) {
    const hit = cache[str];
    return hit || (cache[str] = fn(str))
  }
}

/**
 * Camelize a hyphen-delimited string.
 */
const camelizeRE = /-(\w)/g;
const camelize = cached((str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
});

const SOURCE_KEY = '__data__';

const COMPONENT_LIFECYCLE = {
  created: 'onServiceCreated',
  attached: 'onServiceAttached',
  ready: 'mounted',
  moved: 'moved',
  detached: 'destroyed'
};

const COMPONENT_LIFECYCLE_KEYS = Object.keys(COMPONENT_LIFECYCLE);

const PAGE_LIFETIMES = {
  show: 'onPageShow',
  hide: 'onPageHide',
  resize: 'onPageResize'
};

const PAGE_LIFETIMES_KEYS = Object.keys(PAGE_LIFETIMES);

const PAGE_LIFECYCLE = [
  'onLoad',
  'onShow',
  'onReady',
  'onHide',
  'onUnload',
  'onPullDownRefresh',
  'onReachBottom',
  'onShareAppMessage',
  'onPageScroll',
  'onResize',
  'onTabItemTap'
];

function parsePageMethods (mpComponentOptions, vueComponentOptions) {
  const methods = Object.create(null);
  Object.keys(mpComponentOptions).forEach(key => {
    const value = mpComponentOptions[key];
    if (isFn(value) && PAGE_LIFECYCLE.indexOf(key) === -1) {
      methods[key] = value;
    }
  });
  vueComponentOptions.methods = methods;
}

function parsePageLifecycle (mpComponentOptions, vueComponentOptions) {
  Object.keys(mpComponentOptions).forEach(key => {
    if (PAGE_LIFECYCLE.indexOf(key) !== -1) {
      vueComponentOptions[key] = mpComponentOptions[key];
    }
  });
}

function parsePage (mpComponentOptions) {
  const vueComponentOptions = {
    mixins: [],
    mpOptions: {}
  };

  parseComponents(vueComponentOptions);

  parseData(mpComponentOptions.data, vueComponentOptions);

  parsePageMethods(mpComponentOptions, vueComponentOptions);
  parsePageLifecycle(mpComponentOptions, vueComponentOptions);

  return vueComponentOptions
}

function parseProperties (properties, vueComponentOptions) {
  if (!properties) {
    return
  }
  vueComponentOptions.mpOptions.properties = properties;
}

function parseOptions (options, vueComponentOptions) {
  if (!options) {
    return
  }
  vueComponentOptions.mpOptions.options = options;
}

function parseMethods (methods, vueComponentOptions) {
  if (!methods) {
    return
  }
  if (methods.$emit) {
    console.warn('Method "$emit" conflicts with an existing Vue instance method');
    delete methods.$emit;
  }
  vueComponentOptions.methods = methods;
}

function parseLifecycle (mpComponentOptions, vueComponentOptions) {
  COMPONENT_LIFECYCLE_KEYS.forEach(name => {
    if (hasOwn(mpComponentOptions, name)) {
      (vueComponentOptions[COMPONENT_LIFECYCLE[name]] || (vueComponentOptions[COMPONENT_LIFECYCLE[name]] = []))
        .push(mpComponentOptions[name]);
    }
  });
}

const mpBehaviors = {
  'wx://form-field': {},
  'wx://component-export': {}
};

function callDefinitionFilter (mpComponentOptions) {
  const {
    behaviors,
    definitionFilter
  } = mpComponentOptions;

  const behaviorDefinitionFilters = [];

  if (Array.isArray(behaviors)) {
    behaviors.forEach(behavior => {
      behavior = typeof behavior === 'string' ? mpBehaviors[behavior] : behavior;
      if (behavior.definitionFilter) {
        behaviorDefinitionFilters.push(behavior.definitionFilter);
        behavior.definitionFilter.call(null, mpComponentOptions, []);
      }
    });
  }

  if (isFn(definitionFilter)) {
    return function (defFields) {
      definitionFilter(defFields, behaviorDefinitionFilters);
    }
  }
}

function parseDefinitionFilter (mpComponentOptions, vueComponentOptions) {
  callDefinitionFilter(mpComponentOptions);
}

function parseBehavior (behavior) {
  const {
    data,
    methods,
    behaviors,
    properties
  } = behavior;

  const vueComponentOptions = {
    watch: {},
    mpOptions: {
      mpObservers: []
    }
  };

  parseData(data, vueComponentOptions);
  parseMethods(methods, vueComponentOptions);
  parseBehaviors(behaviors, vueComponentOptions);
  parseProperties(properties, vueComponentOptions);

  parseLifecycle(behavior, vueComponentOptions);
  parseDefinitionFilter(behavior);

  return vueComponentOptions
}

const BEHAVIORS = {
  'wx://form-field': {
    beforeCreate () {
      const mpOptions = this.$options.mpOptions;
      if (!mpOptions.properties) {
        mpOptions.properties = Object.create(null);
      }

      const props = mpOptions.properties;
      // TODO form submit,reset
      if (!hasOwn(props, 'name')) {
        props.name = {
          type: String
        };
      }
      if (!hasOwn(props, 'value')) {
        props.value = {
          type: String // 默认类型调整为 String,否则默认值为 null,导致一些自定义 input 显示不正确
        };
      }
    }
  }
};

function parseBehaviors (behaviors, vueComponentOptions) {
  if (!behaviors) {
    return
  }
  behaviors.forEach(behavior => {
    if (typeof behavior === 'string') {
      BEHAVIORS[behavior] && vueComponentOptions.mixins.push(BEHAVIORS[behavior]);
    } else {
      vueComponentOptions.mixins.push(parseBehavior(behavior));
    }
  });
}

function parseSinglePath (path) {
  return path.split('.')
}

function parseMultiPaths (paths) {
  return paths.split(',').map(path => parseSinglePath(path))
}

function parseObservers (observers, vueComponentOptions) {
  if (!observers) {
    return
  }

  const {
    mpObservers
  } = vueComponentOptions.mpOptions;

  Object.keys(observers).forEach(path => {
    mpObservers.push({
      paths: parseMultiPaths(path),
      observer: observers[path]
    });
  });
}

function relative (from, to) {
  if (to.indexOf('/') === 0) {
    from = '';
  }
  const fromArr = from.split('/');
  const toArr = to.split('/');
  fromArr.pop();
  while (toArr.length) {
    const part = toArr.shift();
    if (part !== '' && part !== '.') {
      if (part !== '..') {
        fromArr.push(part);
      } else {
        fromArr.pop();
      }
    }
  }
  return fromArr.join('/')
}

function parseRelations (relations, vueComponentOptions) {
  if (!relations) {
    return
  }
  Object.keys(relations).forEach(name => {
    const relation = relations[name];
    relation.name = name;
    relation.target = relation.target ? String(relation.target) : relative(global.__wxRoute, name);
  });
  vueComponentOptions.mpOptions.relations = relations;
}

function parseExternalClasses (externalClasses, vueComponentOptions) {
  if (!externalClasses) {
    return
  }
  if (!Array.isArray(externalClasses)) {
    externalClasses = [externalClasses];
  }
  vueComponentOptions.mpOptions.externalClasses = externalClasses;
  if (!vueComponentOptions.mpOptions.properties) {
    vueComponentOptions.mpOptions.properties = Object.create(null);
  }
  externalClasses.forEach(externalClass => {
    vueComponentOptions.mpOptions.properties[camelize(externalClass)] = {
      type: String,
      value: ''
    };
  });
}

function parseLifetimes (lifetimes, vueComponentOptions) {
  if (!lifetimes) {
    return
  }
  parseLifecycle(lifetimes, vueComponentOptions);
}

function parsePageLifetimes (pageLifetimes, vueComponentOptions) {
  if (!pageLifetimes) {
    return
  }
  PAGE_LIFETIMES_KEYS.forEach(key => {
    const lifetimeFn = pageLifetimes[key];
    isFn(lifetimeFn) && (vueComponentOptions[PAGE_LIFETIMES[key]] = lifetimeFn);
  });
}

function parseComponent (mpComponentOptions) {
  const {
    data,
    options,
    methods,
    behaviors,
    lifetimes,
    observers,
    relations,
    properties,
    pageLifetimes,
    externalClasses
  } = mpComponentOptions;

  const vueComponentOptions = {
    mixins: [],
    props: {},
    watch: {},
    mpOptions: {
      mpObservers: []
    }
  };

  parseComponents(vueComponentOptions);

  parseData(data, vueComponentOptions);
  parseOptions(options, vueComponentOptions);
  parseMethods(methods, vueComponentOptions);
  parseBehaviors(behaviors, vueComponentOptions);
  parseLifetimes(lifetimes, vueComponentOptions);
  parseObservers(observers, vueComponentOptions);
  parseRelations(relations, vueComponentOptions);
  parseProperties(properties, vueComponentOptions);
  parsePageLifetimes(pageLifetimes, vueComponentOptions);
  parseExternalClasses(externalClasses, vueComponentOptions);

  parseLifecycle(mpComponentOptions, vueComponentOptions);
  parseDefinitionFilter(mpComponentOptions);

  return vueComponentOptions
}

function initRelationHandlers (type, handler, target, ctx) {
  if (!handler) {
    return
  }
  const name = `_$${type}Handlers`;
  (ctx[name] || (ctx[name] = [])).push(function () {
    handler.call(ctx, target);
  });
}

function initLinkedHandlers (relation, target, ctx) {
  const type = 'linked';
  const name = relation.name;
  const relationNodes = ctx._$relationNodes || (ctx._$relationNodes = Object.create(null));
  (relationNodes[name] || (relationNodes[name] = [])).push(target);
  initRelationHandlers(type, relation[type], target, ctx);
}

function initUnlinkedHandlers (relation, target, ctx) {
  const type = 'unlinked';
  initRelationHandlers(type, relation[type], target, ctx);
}

function findParentRelation (parentVm, target, type) {
  const relations = parentVm &&
    parentVm.$options.mpOptions &&
    parentVm.$options.mpOptions.relations;

  if (!relations) {
    return []
  }
  const name = Object.keys(relations).find(name => {
    const relation = relations[name];
    return relation.target === target && relation.type === type
  });
  if (!name) {
    return []
  }
  return [relations[name], parentVm]
}

function initParentRelation (vm, childRelation, match) {
  const [parentRelation, parentVm] = match(vm, vm.$options.mpOptions.path);
  if (!parentRelation) {
    return
  }

  initLinkedHandlers(parentRelation, vm, parentVm);
  initLinkedHandlers(childRelation, parentVm, vm);

  initUnlinkedHandlers(parentRelation, vm, parentVm);
  initUnlinkedHandlers(childRelation, parentVm, vm);
}

function initRelation (relation, vm) {
  const type = relation.type;
  if (type === 'parent') {
    initParentRelation(vm, relation, function matchParent (vm, target) {
      return findParentRelation(vm.$parent, target, 'child')
    });
  } else if (type === 'ancestor') {
    initParentRelation(vm, relation, function matchAncestor (vm, target) {
      let $parent = vm.$parent;
      while ($parent) {
        const ret = findParentRelation($parent, target, 'descendant');
        if (ret.length) {
          return ret
        }
        $parent = $parent.$parent;
      }
      return []
    });
  }
}

function initRelations (vm) {
  const {
    relations
  } = vm.$options.mpOptions || {};
  if (!relations) {
    return
  }
  Object.keys(relations).forEach(name => {
    initRelation(relations[name], vm);
  });
}

function handleRelations (vm, type) {
  // TODO 需要移除 relationNodes
  const handlers = vm[`_$${type}Handlers`];
  if (!handlers) {
    return
  }
  handlers.forEach(handler => handler());
}

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

function setDataByExprPath (exprPath, value, data) {
  const keys = exprPath.replace(/\[(\d+?)\]/g, '.$1').split('.');
  keys.reduce((obj, key, idx) => {
    if (idx === keys.length - 1) {
      obj[key] = value;
    } else {
      if (typeof obj[key] === 'undefined') {
        obj[key] = {};
      }
      return obj[key]
    }
  }, data);
  return keys.length === 1
}

function setData (data, callback) {
  if (!isPlainObject(data)) {
    return
  }
  Object.keys(data).forEach(key => {
    if (setDataByExprPath(key, data[key], this.data)) {
      !hasOwn(this, key) && proxy(this, SOURCE_KEY, key);
    }
  });
  this.$forceUpdate();
  isFn(callback) && this.$nextTick(callback);
}

/**
 * https://github.com/swan-team/swan-js/blob/61e2a63f7aa576b5daafbe77fdfa7c65b977060c/src/utils/index.js
 */

const _toString$1 = Object.prototype.toString;
/**
 * 深度assign的函数
 * @param {Object} targetObject 要被拷贝的目标对象
 * @param {Object} originObject 拷贝的源对象
 * @return {Object} merge后的对象
 */
const deepAssign = (targetObject = {}, originObject) => {
  const originType = _toString$1.call(originObject);
  if (originType === '[object Array]') {
    targetObject = originObject.slice(0);
    return targetObject
  } else if (originType === '[object Object]') {
    for (const key in originObject) {
      targetObject[key] = deepAssign(targetObject[key], originObject[key]);
    }
    return targetObject
  } else if (originType === '[object Date]') {
    return new Date(originObject.getTime())
  } else if (originType === '[object RegExp]') {
    const target = String(originObject);
    const lastIndex = target.lastIndexOf('/');
    return new RegExp(target.slice(1, lastIndex), target.slice(lastIndex + 1))
  }
  return originObject
};

/**
 * 深度拷贝逻辑，不同于lodash等库，但是与微信一致
 * @param {*} [originObj] 原对象
 * @return {Object|Array} 拷贝结果
 */
const deepClone = originObj => {
  return deepAssign(_toString$1.call(originObj) === '[object Array]' ? [] : {}, originObj)
};

const PROP_DEFAULT_VALUES = {
  [String]: '',
  [Number]: 0,
  [Boolean]: false,
  [Object]: null,
  [Array]: [],
  [null]: null
};

function getDefaultVal (propType) {
  return PROP_DEFAULT_VALUES[propType]
}

function getPropertyVal (options) {
  if (isPlainObject(options)) {
    if (hasOwn(options, 'value')) {
      return options.value
    }
    return getDefaultVal(options.type)
  }
  return getDefaultVal(options)
}

function getType (propOptions) {
  return isPlainObject(propOptions) ? propOptions.type : propOptions
}

function validateProp (key, propsOptions, propsData, vm) {
  let value = propsData[key];
  if (value !== undefined) {
    const propOptions = propsOptions[key];
    const type = getType(propOptions);
    value = formatVal(value, type);
    const observer = propOptions && propOptions.observer;
    if (observer) {
      // 初始化时,异步触发 observer,否则 observer 中无法访问 methods 或其他
      setTimeout(function () {
        observe(observer, vm, value);
      }, 4);
    }
    return value
  }
  return getPropertyVal(propsOptions[key])
}

function formatVal (val, type) {
  if (type === Boolean) {
    return !!val
  } else if (type === String) {
    return String(val)
  }
  return val
}

function observe (observer, vm, newVal, oldVal) {
  try {
    if (typeof observer === 'function') {
      observer.call(vm, newVal, oldVal);
    } else if (typeof observer === 'string' &&
      typeof vm[observer] === 'function'
    ) {
      vm[observer](newVal, oldVal);
    }
  } catch (err) {
    console.error(`execute observer ${observer} callback fail! err: ${err}`);
  }
}

function initProperties (vm, instanceData) {
  const properties = vm.$options.mpOptions.properties;
  if (!properties) {
    return
  }

  const propsData = deepClone(vm.$options.propsData) || {};

  for (const key in properties) {
    const observer = isPlainObject(properties[key]) ? properties[key].observer : false;
    let value = validateProp(key, properties, propsData, vm);
    Object.defineProperty(instanceData, key, {
      enumerable: true,
      configurable: true,
      get () {
        return value
      },
      set (newVal) {
        const oldVal = value;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        // TODO 临时方案,clone array
        value = Array.isArray(newVal) ? newVal.slice(0) : newVal;
        if (observer) {
          observe(observer, vm, newVal, oldVal);
        }
        // 触发渲染
        vm.$forceUpdate();
      }
    });
  }
}

function updateProperties (vm) {
  const properties = vm.$options.mpOptions && vm.$options.mpOptions.properties;
  const propsData = vm.$options.propsData;
  if (propsData && properties) {
    Object.keys(properties).forEach(key => {
      if (hasOwn(propsData, key)) {
        vm[key] = formatVal(propsData[key], getType(properties[key]));
      }
    });
  }
}

function initState (vm) {
  const instanceData = JSON.parse(JSON.stringify(vm.$options.mpOptions.data || {}));

  vm[SOURCE_KEY] = instanceData;

  const propertyDefinition = {
    get () {
      return vm[SOURCE_KEY]
    },
    set (value) {
      vm[SOURCE_KEY] = value;
    }
  };

  Object.defineProperties(vm, {
    data: propertyDefinition,
    properties: propertyDefinition
  });

  vm.setData = setData;

  initProperties(vm, instanceData);

  Object.keys(instanceData).forEach(key => {
    proxy(vm, SOURCE_KEY, key);
  });
}

function initMethods (vm) {
  const oldEmit = vm.$emit;
  vm.triggerEvent = (eventName, detail, options) => {
    const target = {
      dataset: vm.$el.dataset
    };

    const event = {
      target,
      currentTarget: target,
      detail,
      preventDefault: noop,
      stopPropagation: noop
    };

    oldEmit.call(vm, eventName, event);
  };
  // 主要是Vant 自己封装了 $emit,放到 methods 中会触发 Vue 的警告,索性,框架直接重写该方法
  vm.$emit = (...args) => {
    vm.triggerEvent(...args);
  };
  vm.getRelationNodes = (relationKey) => {
    // 需要过滤已被销毁的vm
    /* eslint-disable  no-mixed-operators */
    return (vm._$relationNodes && vm._$relationNodes[relationKey] || []).filter(vm => !vm._isDestroyed)
  };

  vm._$updateProperties = updateProperties;
}

function handleObservers (vm) {
  const watch = vm.$options.watch;
  if (!watch) {
    return
  }
  Object.keys(watch).forEach(name => {
    const observer = watch[name];
    if (observer.mounted) {
      const val = vm[name];
      let handler = observer.handler;
      if (typeof handler === 'string') {
        handler = vm[handler];
      }
      handler && handler.call(vm, val, val);
    }
  });
}

var polyfill = {
  beforeCreate () {
    // 取消 development 时的 Proxy,避免小程序组件模板中使用尚未定义的属性告警
    this._renderProxy = this;

    this._$self = this;
    this._$noop = noop;
  },
  created () { // properties 中可能会访问 methods,故需要在 created 中初始化
    initState(this);
    initMethods(this);
    initRelations(this);
  },
  mounted () {
    handleObservers(this);
  },
  destroyed () {
    handleRelations(this, 'unlinked');
  }
};

global.__wxRoute = '';
global.__wxComponents = Object.create(null);
global.__wxVueOptions = Object.create(null);

function Page (options) {
  const pageOptions = parsePage(options);
  pageOptions.mixins.unshift(polyfill);
  pageOptions.mpOptions.path = global.__wxRoute;
  global.__wxComponents[global.__wxRoute] = pageOptions;
}

function initRelationsHandler (vueComponentOptions) {
  // linked 需要在当前组件 attached 之后再执行
  if (!vueComponentOptions.onServiceAttached) {
    vueComponentOptions.onServiceAttached = [];
  }
  vueComponentOptions.onServiceAttached.push(function onServiceAttached () {
    handleRelations(this, 'linked');
  });
}

function Component (options) {
  const componentOptions = parseComponent(options);
  componentOptions.mixins.unshift(polyfill);
  componentOptions.mpOptions.path = global.__wxRoute;
  initRelationsHandler(componentOptions);
  global.__wxComponents[global.__wxRoute] = componentOptions;
}

function Behavior (options) {
  return options
}

const nextTick = Vue.nextTick;

var index = uni.__$wx__;

export default index;
export { Behavior, Component, Page, nextTick };
