import { observable, autorun, untracked } from 'mobx';
import { styleToCss } from './style';
import { getDeep, generateDataContext } from './util';
import { compLowcodes, create$comp } from './weapp-component';
import EventEmitter from './event-emitter';
import lodashSet from 'lodash.set';

function basePullAt(array, indexes) {
  let length = array ? indexes.length : 0;
  let lastIndex = length - 1;

  // eslint-disable-next-line no-plusplus
  while (length--) {
    let index = indexes[length];
    let previous;
    if (length == lastIndex || index !== previous) {
      previous = index;
      Array.prototype.splice.call(array, index, 1);
    }
  }
  return array;
}

function remove(array, predicate) {
  const result = [];
  if (!array?.length) {
    return result;
  }
  let index = -1;
  const indexes = [];
  const { length } = array;

  while (++index < length) {
    const value = array[index];
    if (predicate(value, index, array)) {
      result.push(value);
      indexes.push(index);
    }
  }
  basePullAt(array, indexes);
  return result;
}

/**
 * convert widget prop to data for wxml
 * @param {*} props
 */
function resolveWidgetProp(props) {
  let { classList = [] } = props;
  const data = {};
  Object.keys(props).forEach((key) => {
    if (props[key] instanceof Function || props[key] === undefined) {
      return;
    }
    data[key] = props[key];
  });
  data.style = styleToCss(props.style);
  data.className = classList.join ? classList.join(' ') : classList;
  const extraProps = [
    'classList',
    '_forItems',
    '_disposers',
    'children',
    'parent',
    '_parentId',
    'id',
    '_order',
    '_scope',
    'widgetType',
    '$comp',
  ];
  extraProps.map((prop) => {
    delete data[prop];
  });
  return data;
}

// widget prop -> wxml data
export function resolveWidgetData(props) {
  if (!Array.isArray(props)) {
    return resolveWidgetProp(props);
  }
  return props.map(resolveWidgetData);
}

export function createWidgets(widgetProps, dataBinds, widgetHolder, ownerMpInst) {
  const rootNode = createWidgetDataTree(widgetProps, dataBinds);
  const failedBinds = [];
  const result = createSubWidgetTree(
    rootNode,
    widgetProps,
    dataBinds,
    ownerMpInst,
    widgetHolder,
    {},
    null,
    failedBinds,
    undefined,
  );
  retryFailedBinds(failedBinds, true);
  return result;
}

/**
 * @param ownerMpInst The MP page or component instance the widgets belongs to
 * @param ownerForWidgetHolder null for the virtual root node(first run)
 * @param curForNode a component node or a virtual root tree node
 * @returns {widgets: {id1:[], id2}, rootWidget: {children: [], _disposers: [], ...otherProps}}
 */
function createSubWidgetTree(
  curForNode,
  widgetProps,
  dataBinds,
  ownerMpInst,
  widgetHolder = {},
  forItems = {},
  ownerForWidgetHolder = null,
  failedBinds = [],
  defaultParent = { children: observable([]), _disposers: [] },
) {
  const indexPostfix = (forItems.lists || [])
    .slice()
    .reverse()
    .map((list) => ID_SEPARATOR + list.currentIndex)
    .join('');

  // traverse down the tree to set up all widgets
  dfsTree(curForNode, (node, parentNode, cache) => {
    const parentForWidgetArr = ownerForWidgetHolder?.[node.id] || [];
    const { _waForKey } = node.value;
    const key = forItems.itemsById?.[node.id]?.[_waForKey];
    const index = cache[parentNode?.id]
      ? cache[parentNode.id].index
      : parentForWidgetArr.findIndex((widget) => key && widget._key === key);
    const existedWidget = index !== -1 ? parentForWidgetArr[index] : null; // try to reuse previous node when rerun for

    if (existedWidget) {
      cache[node.id] = {
        index,
      };
    }

    if (node.forCount === curForNode.forCount) {
      // Leaf node
      let w = existedWidget;
      if (!existedWidget) {
        const parentNode = node.parent;
        let parentWidget = null;
        if (parentNode) {
          parentWidget = widgetHolder[parentNode.id] || ownerForWidgetHolder[parentNode.id];
        }
        w = createAWidget(widgetProps[node.id], node.id, indexPostfix, parentWidget, ownerMpInst);
        w._key = key;
        if (!parentWidget) {
          defaultParent.children.push(w);
        }
        parentForWidgetArr.push(w);
      } else {
        disposeWidget(existedWidget, true);
      }
      setUpWidgetDataBinds(w, dataBinds[node.id], forItems, failedBinds, ownerMpInst.getWeAppInst());
      widgetHolder[node.id] = w;
    } else if (!existedWidget) {
      const len = parentForWidgetArr.push(observable([]));
      widgetHolder[node.id] = parentForWidgetArr[len - 1];
    } else {
      // Reuse existed for widget array
      widgetHolder[node.id] = existedWidget;
    }
  });

  // run for of next level
  dfsTree(curForNode, (node) => {
    if (node.forCount === curForNode.forCount + 1 && dataBinds[node.id] && dataBinds[node.id]._waFor) {
      // find the node bound with next level for
      const parent = node.parent ? widgetHolder[node.parent.id] : defaultParent;
      const dispose = runFor(node, widgetProps, dataBinds, ownerMpInst, forItems, widgetHolder, failedBinds, parent);
      parent._disposers.push(dispose); // Add the for bind dispose to the parent node of forNode
    }
  });

  retryFailedBinds(failedBinds);

  return { widgets: widgetHolder, rootWidget: widgetHolder[curForNode.id] || defaultParent };
}

// Retry failed databinds
function retryFailedBinds(failedBinds, finalTry) {
  const len = failedBinds.length;
  for (let i = 0; i < len; i++) {
    const setUpDataBind = failedBinds.shift();
    setUpDataBind(finalTry);
  }
}

/**
 *
 * @param {*} curForNode
 * @param {*} forItems
 * @param {*} parentForWidgets
 * @param {*} parentWidget
 * @returns top level widgets or for dispose
 */
const _FOR_ERROR_CACHE_MAP = {};
function runFor(
  curForNode,
  widgetProps,
  dataBinds,
  ownerMpInst,
  forItems,
  ownerForWidgetHolder,
  failedBinds,
  defaultParent,
) {
  const nodeId = curForNode.id;
  const { _waForKey } = curForNode.value;

  const dispose = autorun(() => {
    let forList = [];
    try {
      clearTimeout(_FOR_ERROR_CACHE_MAP[nodeId]);

      const $instance = ownerMpInst.getWeAppInst();
      const dataContext = untracked(()=>generateDataContext(defaultParent));

      forList = dataBinds[nodeId]._waFor.call(
        $instance,
        $instance,
        forItems.lists,
        forItems.itemsById,
        undefined,
        dataContext,
      );
      if (!Array.isArray(forList)) {
        forList = [];
      }
    } catch (e) {
      forList = [];
      _FOR_ERROR_CACHE_MAP[nodeId] = setTimeout(() => {
        console.warn('For binding error', nodeId, e);
      }, 1000);
    }

    // Track list change (e.g. push)
    forList.forEach((e) => {});

    untracked(() => {
      // dispose widgets before reused instead
      // disposeWidgets(parentForWidgets[curForNode.id])
      const exsitMap = forList.reduce((map, item) => {
        if (item[_waForKey]) {
          map[item[_waForKey]] = true;
        }
        return map;
      }, {});
      const forWidgets = ownerForWidgetHolder[nodeId];
      const existedWidgetIndex = [];
      const extraWidgetsIndex = [];
      const extraWidgets = [];
      forWidgets.forEach((widget, index) => {
        if (exsitMap[widget._key]) {
          existedWidgetIndex.push(index);
          // need to use uqique key
          exsitMap[widget._key] = undefined;
        } else {
          extraWidgetsIndex.push(index);
        }
      });
      const extraWidgetsIndexMap = extraWidgetsIndex.reduce((map, item) => {
        map[item] = true;
        extraWidgets.push(forWidgets[item]);
        return map;
      }, {});

      // clean extra widgets of previous for run
      dfsTree(curForNode, (node) => {
        const arr = ownerForWidgetHolder[node.id];
        remove(arr, (_, index) => {
          return extraWidgetsIndexMap[index];
        });
      });

      // 清理根 for 的 autorun, 并递归清理子节点
      extraWidgets.map((w) => {
        disposeWidget(w);
        const { children } = w.parent || defaultParent;
        children.remove(w);
        // w.parent = null
      });

      forList.forEach((item, index) => {
        let { lists = [], itemsById = {} } = forItems;
        const _forItems = {
          lists: [{ currentItem: item, currentIndex: index }, ...lists],
          itemsById: { ...itemsById, [nodeId]: item },
        };
        const { rootWidget } = createSubWidgetTree(
          curForNode,
          widgetProps,
          dataBinds,
          ownerMpInst,
          {},
          _forItems,
          ownerForWidgetHolder,
          failedBinds,
          defaultParent,
        );
        rootWidget._forItems = _forItems;
      });
    });
  });

  return dispose;
}

function createAWidget(props, nodeId, indexPostfix, parent, ownerMpInst) {
  const w = observable(props);
  const id = `${nodeId}${indexPostfix}`;

  // Builtin props
  Object.defineProperty(w, 'id', { value: id });
  const { widgetType } = w;
  delete w.widgetType;
  Object.defineProperty(w, 'widgetType', { value: widgetType });
  Object.defineProperty(w, '_scope', {
    value: observable({
      id: nodeId,
      _parent: null,
      dataContext: {},
    }),
  });

  // w._disposers = []
  // w.children = []
  Object.defineProperty(w, 'children', { value: observable([]) });
  Object.defineProperty(w, '_disposers', { value: observable([]) });
  Object.defineProperty(w, '_eventListeners', { value: new EventEmitter() });
  if (parent) {
    // w.parent = parent
    Object.defineProperty(w, 'parent', { value: parent });
    w._scope.parent = parent._scope;
    parent.children.push(w);
  }
  delete w._parentId;

  Object.defineProperty(w, '$comp', { value: create$comp(w) });
  mountBuiltinWigetsAPI(w, ownerMpInst);
  return w;
}

function setUpWidgetDataBinds(w, dataBinds, forItems, failedBinds, ctx) {
  Object.keys(dataBinds || {})
    .sort((a, b) => {
      return a.length - b.length > 0 ? 1 : -1;
    })
    .map((prop) => {
      if (prop === '_waFor') {
        return;
      }
      let timer = null;
      const setUpDataBind = (isFinalTry) => {
        let ran = false;
        const dispose = autorun((reaction) => {
          try {
            clearTimeout(timer);

            const dataContext = untracked(()=>generateDataContext(w));

            // Computed data bind in the next tick since data bind may read widgets data
            const value = dataBinds[prop].call(ctx, ctx, forItems.lists, forItems.itemsById, undefined, dataContext);
            const paths = prop.split('.').filter((key) => !!key);
            if (paths.length > 1) {
              // 一定要 untracked 不然爆栈了
              untracked(() => lodashSet(w, prop, value));
            } else {
              // 普通 key 直接赋值
              w[prop] = value;
            }
          } catch (e) {
            if (prop === '_waIf') {
              w[prop] = false;
            }

            if (isFinalTry || ran) {
              timer = setTimeout(() => {
                console.warn(`Error computing data bind ${w.id}.${prop}`, e);
              }, 1000);
            } else {
              reaction.dispose();
              failedBinds.push(setUpDataBind);
            }

            ran = true;
          }
        });
        w._disposers.push(dispose);
      };
      setUpDataBind();
    });
}

export function findForItemsOfWidget(widget) {
  const forItems = widget._forItems;
  if (forItems) return forItems;
  if (widget.parent) return findForItemsOfWidget(widget.parent);
}

export const ID_SEPARATOR = '-';
export function getWidget(widgets, id) {
  return getDeep(widgets, id, ID_SEPARATOR);
}

/**
 * Add parent, children to widget
 */
function createWidgetDataTree(widgets, dataBinds) {
  const virtualRoot = { children: [], forCount: 0 };
  const nodes = Object.keys(widgets).reduce((result, id) => {
    const w = widgets[id];
    result[id] = { id, value: w, _order: w._order, children: [], parent: null, forCount: 0 };
    return result;
  }, {});

  // Create widgets tree API
  Object.keys(nodes).map((id) => {
    const curNode = nodes[id];
    const parent = nodes[widgets[id]._parentId];
    // delete widgets[id]._parentId
    if (!parent) {
      virtualRoot.children.push(curNode);
      return;
    }
    curNode.parent = parent;
    parent.children.push(curNode);
  });

  // Sort children
  Object.keys(nodes).map((id) => {
    nodes[id].children.sort((a, b) => a._order - b._order);
  });

  virtualRoot.children.map(addForCount);

  // dfs, add forCount
  function addForCount(node) {
    if (node.parent) {
      node.forCount = node.parent.forCount;
    }
    if (dataBinds[node.id]?._waFor) {
      node.forCount += 1;
    }
    node.children.map(addForCount);
  }

  return virtualRoot;
}

function dfsTree(node, fn, parent, cache = {}) {
  node.value && fn(node, parent, cache);
  node.children.map((e) => dfsTree(e, fn, node.value ? node : null, cache));
}

// dispose autorun, widget can be the virtual root widget
export function disposeWidget(widget, noRecursive) {
  const disposers = widget._disposers;
  disposers.map((dispose) => dispose());
  disposers.splice(0, disposers.length);
  !noRecursive && widget.children.forEach((w) => disposeWidget(w));
}

export function createInitData(widgets, dataBinds, keyPrefix = '') {
  return Object.keys(widgets).reduce((result, id) => {
    if (!isWidgetInFor(id, widgets, dataBinds)) {
      result[keyPrefix + id] = resolveWidgetData(widgets[id]);
    } else {
      result[keyPrefix + id] = [];
    }
    return result;
  }, {});
}

function isWidgetInFor(id, widgets, dataBinds) {
  let curNode = widgets[id];
  let nodeId = id;
  while (curNode) {
    if (dataBinds[nodeId]?._waFor) {
      return true;
    }
    nodeId = curNode._parentId;
    curNode = widgets[nodeId];
  }
}

function mountBuiltinWigetsAPI(widget, owner) {
  // #1 builtin APIs
  widget.findWidgets = function (filter, includeInvisibleDescendants) {
    let { children = [] } = this;
    if (!includeInvisibleDescendants) {
      // include visible widgets only by default
      children = children.filter((e) => e._waIf !== false);
    }
    const matched = [];
    children.forEach((w) => {
      if (filter(w)) {
        matched.push(w);
      }
      matched.push(...w.findWidgets(filter, includeInvisibleDescendants));
    });
    return matched;
  };

  widget.getWidgetsByType = function (type, includeInvisibleDescendants) {
    return this.findWidgets((w) => w.widgetType === type, includeInvisibleDescendants);
  };

  /**
   * Similar to selectOwnerComponent of WX MP: https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
   */
  widget.getOwnerWidget = function () {
    return owner?.getWeAppInst?.()?.node;
  };

  // Will be overwritten by composited component
  widget.getDom = function (fields) {
    return new Promise((resolve, reject) => {
      const query = (owner || wx).createSelectorQuery();
      query
        .select(`#${this.id}`)
        .fields(fields, (res) => {
          resolve(res);
        })
        .exec();
    });
  };

  widget.on = function (type, listener) {
    this._eventListeners.on(type, listener);
  };

  widget.off = function (type, listener) {
    this._eventListeners.off(type, listener);
  };

  widget.getConfig = () => ({});

  const lowcode = compLowcodes[widget.widgetType];
  if (lowcode) {
    const { index = {}, config } = lowcode;
    const builtinProps = Object.keys(widget.$comp);

    widget.getConfig = () => config;

    // #2 User defined APIs
    const { publicMethods = {} } = index;
    Object.keys(publicMethods).map((name) => {
      if (builtinProps.indexOf(name) > -1) {
        console.error(
          `Invalid publicMethod name(${name}) of Component(${widget.widgetType}), builtin name can't be used, please rename this method.`,
        );
        return;
      }
      const method = publicMethods[name];
      if (method instanceof Function) {
        Object.defineProperty(widget, name, { value: method.bind(widget.$comp) });
        Object.defineProperty(widget.$comp, name, { value: method });
      } else {
        console.error(`Component(${widget.widgetType}) method(${name}) is not a function.`);
      }
    });
  }

  widget._methods = {};
}
