import * as React from 'react';
import { observable, autorun, untracked, toJS } from 'mobx';
import { remove, set as lodashSet, get as lodashGet, isEqual } from 'lodash-es';
import { EventProxy } from './eventProxy';
import { runtime } from './adapter';

export const widgetKeys = [
  'getConfig',
  'findWidgets',
  'getWidgetsByType',
  'extends',
  'addWidgetsListener',
  'removeWidgetsListener',
  'getOwnerWidget',
  'getDom',
  'domRef',
  '_getInstanceRef',
];

export const WidgetsContext = React.createContext({ parent: null });

export function checkVisible({ _visible }) {
  return _visible !== false && _visible !== '';
}

// 实现和小程序一致的 API，以兼容多端
// widget.getDom({ rect: true })
// https://developers.weixin.qq.com/miniprogram/dev/api/wxml/NodesRef.fields.html

interface MPNodesRefFields {
  id?: HTMLElement['id'];
  dataset?: HTMLElement['dataset'];
  // rect
  left?: DOMRect['left'];
  right?: DOMRect['right'];
  top?: DOMRect['top'];
  bottom?: DOMRect['bottom'];

  // size
  width?: DOMRect['width'];
  height?: DOMRect['height'];

  // scrollOffest
  scrollLeft?: HTMLElement['scrollLeft'];
  scrollTop?: HTMLElement['scrollTop'];

  // html attribute， css attribute
  [prop: string]: string | any;
}

export function getDom(element: HTMLElement, options): Promise<MPNodesRefFields> {
  if (!element) {
    console.warn('getDom 接口未传入有效的 element');
    return Promise.resolve({});
  }

  let result: MPNodesRefFields = {};
  if (options.id) result.id = element.id;
  if (options.dataset) result.dataset = element.dataset;
  if (options.rect) {
    const rect = element.getBoundingClientRect();
    Object.assign(result, {
      left: rect.left,
      right: rect.right,
      top: rect.top,
      bottom: rect.bottom,
    });
  }
  if (options.size) {
    const rect = element.getBoundingClientRect();
    Object.assign(result, {
      width: rect.width,
      height: rect.height,
    });
  }
  if (options.scrollOffset) {
    Object.assign(result, {
      scrollLeft: element.scrollLeft,
      scrollTop: element.scrollTop,
    });
  }
  if (options.properties && Array.isArray(options.properties)) {
    options.properties.forEach((propName) => {
      result[propName] = element.getAttribute(propName);
    });
  }
  if (options.computedStyle && Array.isArray(options.computedStyle)) {
    const computedStyle = runtime.getComputedStyle(element);
    options.computedStyle.forEach((propName) => {
      result[propName] = computedStyle[propName];
    });
  }

  return Promise.resolve(result);
}

export function findWidgets(widget, parentType, filterFn, includeInvisibleDescendants) {
  if (!widget) return [];

  let { children = [] } = widget;
  let matched = children.filter(filterFn);

  // 过滤掉不可见后代
  if (!includeInvisibleDescendants) {
    children = children.filter((item) => checkVisible(item));
  }

  children.forEach((child) => {
    // 如果递归过程中发现了自己，则停止递归
    matched = matched.concat(findWidgets(child, parentType, filterFn, includeInvisibleDescendants));
  });

  // 过滤掉不可见的匹配项
  if (!includeInvisibleDescendants) {
    matched = matched.filter((item) => checkVisible(item));
  }

  return matched;
}

const retryQueue = [];
// 递归执行
export function retryDataBinds(tryTime = 10) {
  const fn = retryQueue.shift();
  if (!fn || tryTime <= 0) return;
  try {
    fn({ silent: tryTime > 1 });
  } catch (e) {
    console.error('retryDataBinds', e);
  }
  retryDataBinds(tryTime - 1);
}
export function createWidgets(widgetProps, dataBinds, context) {
  const nodeTree = createWidgetTree(widgetProps, dataBinds);
  const widgets = runFor(nodeTree, {}, null, null, context);
  return widgets;

  /**
   *
   * @param {*} curForNode
   * @param {*} forItems
   * @param {*} parentLevelWidgets
   * @param {*} parentWidget
   * @returns top level widgets or for dispose
   */
  function runFor(curForNode, forItems, parentLevelWidgets, parentWidget, context) {
    const nodeId = curForNode.id;
    if (!curForNode.value) {
      return createSubTree(curForNode, {}, context);
    }
    const { _waForKey } = curForNode.value;
    const timer = {};
    const dispose = autorun(() => {
      let forList = [];
      try {
        clearTimeout(timer[nodeId]);
        const list = dataBinds[nodeId]._waFor(forItems, undefined, context);
        if (!Array.isArray(list)) {
          timer[nodeId] = setTimeout(() => {
            console.warn(nodeId, 'For 循环绑定的数据并不是数组，请检查');
          }, 1000);
        } else {
          forList = list;
        }
      } catch (e) {
        timer[nodeId] = setTimeout(() => {
          console.error('waFor error', e);
        }, 1000);
      }

      // 让 forList 进行监听
      forList.forEach(() => {});
      untracked(() => {
        // dispose widgets before reused instead
        // disposeWidgets(parentLevelWidgets[curForNode.id]);

        const exsitMap = forList.reduce((map, item) => {
          if (item?.[_waForKey]) {
            map[item[_waForKey]] = true;
          }
          return map;
        }, {});
        const forWidgets = parentLevelWidgets?.[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 nodes of previouse for run
        dfsTree(
          curForNode,
          (node) => {
            const arr = parentLevelWidgets[node.id];
            remove(arr, (_, index) => {
              return extraWidgetsIndexMap[index];
            });
          },
          undefined,
        );

        // 清理根 for 的 autorun, 并递归清理子节点
        extraWidgets.map((w) => {
          disposeWidgets([w]);
          parentWidget && remove(parentWidget.children, ({ id }) => id === nodeId);
        });

        forList.forEach((item, index) => {
          const subForItems = { ...forItems, [nodeId]: item, index: { ...forItems.index, [nodeId]: index } };
          createSubTree(curForNode, subForItems, context);
        });

        // 非初始化时遇到需要重新构建 dataBinds
        retryDataBinds();
      });
    });

    return dispose;

    function createSubTree(curForNode, subForItems, context) {
      const widgets = {};

      // traverse down the tree to set all widgets
      dfsTree(
        curForNode,
        (node, parentNode, cache) => {
          const parentForWidgetArr = parentLevelWidgets?.[node.id] || [];
          const { _waForKey } = node.value;
          const key = subForItems[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;

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

          if (node.forCount === curForNode.forCount) {
            // Leaf node
            let w = existedWidget;

            if (!existedWidget) {
              w = observable(widgetProps[node.id]);
              w.id = node.id;
              w._key = key;
              if (node === curForNode) {
                w._disposers = [];
              }
              w._listeners = new EventProxy();
              // 提供一个给 Node 挂载 API 的方式
              untracked(() => {
                if (!w.getConfig) {
                  // 兼容非复合组件调用 getConfig 能力
                  w.getConfig = () => ({});
                }
                w.findWidgets = (type, includeInvisibleDescendants) =>
                  findWidgets(w, w.widgetType, type, includeInvisibleDescendants);
                w.getWidgetsByType = (type, includeInvisibleDescendants) =>
                  w.findWidgets((currentWidget) => currentWidget.widgetType === type, includeInvisibleDescendants);
                w.extends = (name, fnOrData) =>
                  Object.defineProperty(w, name, {
                    value: fnOrData,
                    writable: true,
                  });
                w.addWidgetsListener = (name, listener) => w._listeners.on(name, listener);
                w.removeWidgetsListener = (name, listener) => w._listeners.cancel(name, listener);
                w.getOwnerWidget = () => null; // 默认带上一个 widget
                w._getInstanceRef = () => null; // 默认初始值
                Object.defineProperty(w, '_methods', {
                  get() {
                    const instance = w._getInstanceRef();
                    return instance?.current.methods;
                  },
                });
              });
              w.children = [];
              const parent = parentNode ? widgets[parentNode.id] : parentWidget;
              if (parent) {
                w.parent = parent;
                // 只有可显示 visible 的才存入 children 里
                if (checkVisible(w)) {
                  parent.children.push(w);
                }
              }
              parentForWidgetArr.push?.(w);
            } else {
              disposeWidgets([existedWidget], true);
            }
            widgets[node.id] = w;
            // Setup data binds
            Object.keys(dataBinds[node.id] || {})
              .sort((a, b) => {
                return a.length - b.length > 0 ? 1 : -1;
              })
              .map((prop) => {
                if (prop === '_waFor') {
                  return;
                }
                let timer = null;
                function getBindData(options = { silent: false }) {
                  let disposeError = false;
                  const dispose = autorun(() => {
                    try {
                      clearTimeout(timer);
                      // Computed data bind in the next tick since data bind may read widgets data
                      const value = dataBinds[node.id][prop](subForItems, undefined, context);
                      const paths = prop.split('.').filter((key) => !!key);
                      if (paths.length > 1) {
                        // 一定要 untracked 不然爆栈了
                        untracked(() => {
                          const pre = lodashGet(w, prop);
                          lodashSet(w, prop, value);
                          /**
                           * 深层的对象内部值变化不会触发 obverver，因此浅拷贝对象
                           */
                          if (Array.isArray(w[paths[0]])) {
                            if (!isEqual(pre, value)) {
                              w[paths[0]] = [...w[paths[0]]];
                            }
                          }
                        });
                      } else {
                        // 普通 key 直接赋值
                        w[prop] = value;
                      }
                      disposeError = false;
                    } catch (e) {
                      if (prop === '_visible') {
                        w[prop] = false;
                      } else {
                        retryQueue.push(getBindData);
                        disposeError = true;
                      }
                      if (!options?.silent) {
                        timer = setTimeout(() => {
                          console.warn(`Error computing data bind ${w.id}.${prop}`, e);
                          timer = null;
                        }, 1000);
                      }
                    }
                  });
                  if (!disposeError && curForNode.id) {
                    widgets[curForNode.id]._disposers.push(dispose);
                  }
                }
                getBindData();
              });
          } else if (!existedWidget) {
            if (parentLevelWidgets) {
              const len = parentLevelWidgets[node.id].push([]);
              widgets[node.id] = parentLevelWidgets[node.id][len - 1];
            } else {
              widgets[node.id] = observable([]);
            }
          } else {
            widgets[node.id] = existedWidget;
          }
        },
        undefined,
      );

      // run for of next level
      dfsTree(
        curForNode,
        (node, parentNode) => {
          if (node.forCount === curForNode.forCount + 1 && dataBinds[node.id] && dataBinds[node.id]._waFor) {
            widgets[node.id]._disposers = { dataBinds: [] };
            const dispose = runFor(node, subForItems, widgets, node.parent && widgets[node.parent.id], context);
            curForNode.id && widgets[curForNode.id]._disposers.push(dispose);
          }
        },
        undefined,
      );

      return widgets;
    }
  }
}

/**
 * Add parent, children to widget
 */
function createWidgetTree(widgets, dataBinds) {
  const virtualRoot = { children: [], forCount: 0 };
  const nodes = Object.keys(widgets).reduce((result, id) => {
    result[id] = {
      id,
      value: widgets[id],
      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];
    if (!parent) {
      virtualRoot.children.push(curNode);
      return;
    }
    curNode.parent = parent;
    parent.children.push(curNode);
  });

  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++;
    }
    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
function disposeWidgets(widgets = [], noRecursive = false) {
  widgets.forEach((widget) => {
    const disposers = widget._disposers;
    if (disposers) {
      disposers.map((dispose) => dispose());
      disposers.splice(0, disposers.length);
    }
    if (!noRecursive) {
      disposeWidgets(widget.children);
    }
  });
}
