import React from 'react';
import qs from 'qs';
import {getEnv, destroy} from 'mobx-state-tree';
import find from 'lodash/find';
import {observer} from 'mobx-react';

import {wrapFetcher} from './utils/api';
import {findIndex, promisify, string2regExp} from './utils/helper';
import {normalizeLink} from './utils/normalizeLink';
import {
  EventListeners,
  createRendererEvent,
  RendererEventListener,
  OnEventProps,
  RendererEvent
} from './utils/renderer-event';

import {getTheme, ThemeInstance, ThemeProps} from './theme';
import {makeTranslator, LocaleProps, correctionLocale} from './locale';
import {RendererStore, IRendererStore, IIRendererStore} from './store/index';
import ScopedRootRenderer, {RootRenderProps} from './Root';
import Scoped from './Scoped';
import {HocStoreFactory} from './WithStore';
import {EnvContext} from './env';
import {runActions} from './actions/Action';

import Alert from './components/Alert2';
import {toast} from './components/Toast';
import {alert, confirm, setRenderSchemaFn} from './components/Alert';

import type {RendererEnv} from './env';
import type {IScopedContext} from './Scoped';
import {Api, fetcherResult, Payload, SchemaNode, Schema, Action, EventTrack, PlainObject} from './types';

export interface TestFunc {
  (
    path: string,
    schema?: Schema,
    resolveRenderer?: (
      path: string,
      schema?: Schema,
      props?: any
    ) => null | RendererConfig
  ): boolean;
}

export interface RendererBasicConfig {
  test?: RegExp | TestFunc;// 正则匹配多个名字指向一个渲染器
  type?: string;// 渲染器名
  name?: string;// 渲染器名 配合test用
  storeType?: string;// 当前渲染器依赖的 store
  shouldSyncSuperStore?: (// 有storeType才会用 控制didupdate生命周期 是否更新store.data数据 返回false 不更新
    store: any,
    props: any,
    prevProps: any
  ) => boolean | undefined;
  storeExtendsData?: boolean | ((props: any) => boolean); // 有storeType才会用 是否需要继承上层数据。
  weight?: number; // 权重，值越低越优先命中。
  isolateScope?: boolean;// 为真 套一层Scoped HOC
  isFormItem?: boolean;
  // [propName:string]:any;
}

export interface RendererProps extends ThemeProps, LocaleProps {
  render: (region: string, node: SchemaNode, props?: any) => JSX.Element;
  env: RendererEnv;
  $path: string; // 当前组件所在的层级信息
  $schema: any; // 原始 schema 配置
  store?: IIRendererStore;
  syncSuperStore?: boolean;
  data: {
    [propName: string]: any;
  };
  defaultData?: object;
  className?: any;
  [propName: string]: any;
}

export type RendererComponent = React.ComponentType<RendererProps> & {
  propsList?: Array<any>;
};

export interface RendererConfig extends RendererBasicConfig {
  component: RendererComponent;
  Renderer?: RendererComponent; // 原始组件
}

export interface RenderSchemaFilter {
  (schema: Schema, renderer: RendererConfig, props?: any): Schema;
}

export interface RenderOptions {
  session?: string;
  fetcher?: (config: fetcherConfig) => Promise<fetcherResult>;
  wsFetcher?: (
    ws: string,
    onMessage: (data: any) => void,
    onError: (error: any) => void
  ) => void;
  isCancel?: (value: any) => boolean;
  notify?: (
    type: 'error' | 'success',
    msg: string,
    conf?: {
      closeButton?: boolean;
      timeout?: number;
    }
  ) => void;
  jumpTo?: (to: string, action?: Action, ctx?: object) => void;
  alert?: (msg: string) => void;
  confirm?: (msg: string, title?: string) => boolean | Promise<boolean>;
  rendererResolver?: (
    path: string,
    schema: Schema,
    props: any
  ) => null | RendererConfig;
  copy?: (contents: string) => void;
  getModalContainer?: () => HTMLElement;
  loadRenderer?: (
    schema: Schema,
    path: string,
    reRender: Function
  ) => Promise<React.ReactType> | React.ReactType | JSX.Element | void;
  affixOffsetTop?: number;
  affixOffsetBottom?: number;
  [propName: string]: any;
}

export interface fetcherConfig {
  url: string;
  method?: 'get' | 'post' | 'put' | 'patch' | 'delete';
  data?: any;
  config?: any;
}

const renderers: Array<RendererConfig> = [];// 存已经注册的渲染器集合 按渲染器的weight升序
const rendererNames: Array<string> = [];// 存已近注册的渲染器名集合 为了防止重复注册用
const schemaFilters: Array<RenderSchemaFilter> = [];
let anonymousIndex = 1;

export function addSchemaFilter(fn: RenderSchemaFilter) {
  schemaFilters.push(fn);
}

export function filterSchema(
  schema: Schema,
  render: RendererConfig,
  props?: any
) {
  return schemaFilters.reduce(
    (schema, filter) => filter(schema, render, props),
    schema
  ) as Schema;
}

export function Renderer(config: RendererBasicConfig) {
  return function <T extends RendererComponent>(component: T): T {
    const renderer = registerRenderer({
      ...config,
      component: component
    });
    return renderer.component as T;
  };
}

export function registerRenderer(config: RendererConfig): RendererConfig {
  if (!config.test && !config.type) {
    throw new TypeError('please set config.test or config.type');
  } else if (!config.component) {
    throw new TypeError('config.component is required');
  }

  if (typeof config.type === 'string' && config.type) {
    config.type = config.type.toLowerCase();
    config.test =
      config.test || new RegExp(`(^|\/)${string2regExp(config.type)}$`, 'i');
  }

  config.weight = config.weight || 0;
  config.Renderer = config.component;
  config.name = config.name || config.type || `anonymous-${anonymousIndex++}`;

  if (~rendererNames.indexOf(config.name)) {
    throw new Error(
      `The renderer with name "${config.name}" has already exists, please try another name!`
    );
  }

  if (config.storeType) {
    config.component = HocStoreFactory({
      storeType: config.storeType,
      extendsData: config.storeExtendsData,
      shouldSyncSuperStore: config.shouldSyncSuperStore
    })(observer(config.component));
  }

  if (config.isolateScope) {
    config.component = Scoped(config.component);
  }

  const idx = findIndex(
    renderers,
    item => (config.weight as number) < item.weight
  );
  ~idx ? renderers.splice(idx, 0, config) : renderers.push(config);
  rendererNames.push(config.name);
  return config;
}

export function unRegisterRenderer(config: RendererConfig | string) {
  let idx =
    typeof config === 'string'
      ? findIndex(renderers, item => item.name === config)
      : renderers.indexOf(config);
  ~idx && renderers.splice(idx, 1);

  let idx2 =
    typeof config === 'string'
      ? findIndex(rendererNames, item => item === config)
      : rendererNames.indexOf(config.name || '');
  ~idx2 && rendererNames.splice(idx2, 1);

  // 清空渲染器定位缓存
  cache = {};
}

export function loadRenderer(schema: Schema, path: string) {
  return (
    <Alert level="danger">
      <p>Error: 找不到对应的渲染器</p>
      <p>Path: {path}</p>
      <pre>
        <code>{JSON.stringify(schema, null, 2)}</code>
      </pre>
    </Alert>
  );
}

const defaultOptions: RenderOptions = {
  session: 'global',
  affixOffsetTop: 50,
  affixOffsetBottom: 0,
  loadRenderer,
  fetcher() {
    return Promise.reject('fetcher is required');
  },
  // 使用 WebSocket 来实时获取数据
  wsFetcher(ws, onMessage, onError) {
    if (ws) {
      const socket = new WebSocket(ws);
      socket.onmessage = (event: any) => {
        if (event.data) {
          onMessage(JSON.parse(event.data));
        }
      };
      socket.onerror = onError;
      return {
        close: socket.close
      };
    } else {
      return {
        close: () => {}
      };
    }
  },
  isCancel() {
    return false;
  },
  updateLocation() {
  },
  alert,
  confirm,
  notify: (type, msg, conf) =>
    toast[type]
      ? toast[type](msg, type === 'error' ? 'Error' : 'Info', conf)
      : console.warn('[Notify]', type, msg),

  jumpTo: (to: string, action?: any) => {
    if (to === 'goBack') {
      return window.history.back();
    }
    to = normalizeLink(to);
    if (action && action.actionType === 'url') {
      action.blank === false ? (window.location.href = to) : window.open(to);
      return;
    }
    if (/^https?:\/\//.test(to)) {
      window.location.replace(to);
    } else {
      location.href = to;
    }
  },
  isCurrentUrl: (to: string) => {
    if (!to) {
      return false;
    }

    const link = normalizeLink(to);
    const location = window.location;
    let pathname = link;
    let search = '';
    const idx = link.indexOf('?');
    if (~idx) {
      pathname = link.substring(0, idx);
      search = link.substring(idx);
    }
    if (search) {
      if (pathname !== location.pathname || !location.search) {
        return false;
      }
      const query = qs.parse(search.substring(1));
      const currentQuery = qs.parse(location.search.substring(1));
      return Object.keys(query).every(key => query[key] === currentQuery[key]);
    } else if (pathname === location.pathname) {
      return true;
    }
    return false;
  },
  copy(contents: string) {
    console.error('copy contents', contents);
  },
  // 用于跟踪用户在界面中的各种操作
  tracker(eventTrack: EventTrack, props: PlainObject) {},
  // 返回解绑函数
  bindEvent(renderer: any) {
    if (!renderer) {
      return undefined;
    }
    const listeners: EventListeners = renderer.props.$schema.onEvent;
    if (listeners) {
      // 暂存
      for (let key of Object.keys(listeners)) {
        this.rendererEventListeners.push({
          renderer,
          type: key,
          weight: listeners[key].weight || 0,
          actions: listeners[key].actions
        });
      }

      return () => {
        this.rendererEventListeners = this.rendererEventListeners.filter(
          (item: RendererEventListener) => item.renderer !== renderer
        );
      };
    }

    return undefined;
  },
  async dispatchEvent(
    e: string | React.MouseEvent<any>,
    renderer: React.Component<RendererProps>,
    scoped: IScopedContext,
    data: any,
    broadcast?: RendererEvent<any>
  ) {
    const eventName = typeof e === 'string' ? e : e.type;
    if (!broadcast) {
      const eventConfig = renderer?.props?.onEvent?.[eventName];

      if (!eventConfig) {
        // 没命中也没关系
        return Promise.resolve(undefined);
      }
    }

    // 没有可处理的监听
    if (!this.rendererEventListeners.length) {
      return Promise.resolve();
    }

    // 如果是广播动作，就直接复用
    const rendererEvent =
      broadcast ||
      createRendererEvent(eventName, {
        env: this,
        nativeEvent: e,
        data,
        scoped
      });

    // 过滤&排序
    const listeners = this.rendererEventListeners
      .filter(
        (item: RendererEventListener) =>
          item.type === eventName &&
          (broadcast ? true : item.renderer === renderer)
      )
      .sort(
        (prev: RendererEventListener, next: RendererEventListener) =>
          next.weight - prev.weight
      );

    for (let listener of listeners) {
      await runActions(listener.actions, listener.renderer, rendererEvent);

      // 停止后续监听器执行
      if (rendererEvent.stoped) {
        break;
      }
    }

    return rendererEvent;
  },
  rendererResolver: resolveRenderer,
  replaceTextIgnoreKeys: [
    'type',
    'name',
    'mode',
    'target',
    'reload',
    'persistData'
  ],
  /**
   * 过滤 html 标签，可用来添加 xss 保护逻辑
   */
  filterHtml: (input: string) => input
};
let stores: {
  [propName: string]: IRendererStore;
} = {};
export function render(
  schema: Schema,
  props: RootRenderProps = {},
  options: RenderOptions = {},
  pathPrefix: string = ''
): JSX.Element {
  let locale = correctionLocale(props.locale);
  
  const translate = props.translate || makeTranslator(locale);
  let store = stores[options.session || 'global'];

  if (!store) {
    options = {
      ...defaultOptions,
      ...options,
      fetcher: options.fetcher
        ? wrapFetcher(options.fetcher)
        : defaultOptions.fetcher,
      confirm: promisify(
        options.confirm || defaultOptions.confirm || window.confirm
      ),
      locale,
      translate
    } as any;

    store = RendererStore.create({}, options);
    stores[options.session || 'global'] = store;
  }

  (window as any).pandoraStore = store;
  const env = getEnv(store);

  const theme = props.theme || 'pandora';
  env.theme = getTheme(theme);

  return (
    <EnvContext.Provider value={env}>
      <ScopedRootRenderer
        {...props}
        schema={schema}
        pathPrefix={pathPrefix}
        rootStore={store}
        env={env}
        theme={theme}
        locale={locale}
        translate={translate}
      />
    </EnvContext.Provider>
  );
}

// 默认 env 会被缓存，所以新传入的 env 不会替换旧的。
// 除非先删了旧的，新的才会生效。
export function clearStoresCache(
  sessions: Array<string> | string = Object.keys(stores)
) {
  if (!Array.isArray(sessions)) {
    sessions = [sessions];
  }

  sessions.forEach(key => {
    const store = stores[key];

    // @ts-ignore
    delete stores[key];

    store && destroy(store);
  });
}

// 当然也可以直接这样更新。
// 主要是有时候第一次创建的时候并没有准备多少接口，
// 可以后续补充点，比如 amis 自己实现的，prompt 里面的表单。
export function updateEnv(options: Partial<RenderOptions>, session = 'global') {
  options = {
    ...options
  };

  if (options.fetcher) {
    options.fetcher = wrapFetcher(options.fetcher) as any;
  }

  if (options.confirm) {
    options.confirm = promisify(options.confirm);
  }

  let store = stores[options.session || session];
  if (!store) {
    store = RendererStore.create({}, options);
    stores[options.session || session] = store;
  } else {
    const env = getEnv(store);
    Object.assign(env, options);
  }
}

let cache: {[propName: string]: RendererConfig} = {};
export function resolveRenderer(
  path: string,
  schema?: Schema
): null | RendererConfig {
  const type = typeof schema?.type == 'string' ? schema.type.toLowerCase() : '';

  if (type && cache[type]) {
    return cache[type];
  } else if (cache[path]) {
    return cache[path];
  } else if (path && path.length > 1024) {
    throw new Error('Path太长是不是死循环了？');
  }

  let renderer: null | RendererConfig = null;

  renderers.some(item => {
    let matched = false;

    // 直接匹配类型，后续注册渲染都应该用这个方式而不是之前的判断路径。
    if (item.type && type) {
      matched = item.type === type;

      // 如果是type来命中的，那么cache的key直接用 type 即可。
      if (matched) {
        cache[type] = item;
      }
    } else if (typeof item.test === 'function') {
      matched = item.test(path, schema, resolveRenderer);
    } else if (item.test instanceof RegExp) {
      matched = item.test.test(path);
    }

    if (matched) {
      renderer = item;
    }

    return matched;
  });

  if (
    renderer !== null &&
    ((renderer as RendererConfig).type ||
      (renderer as RendererConfig).test instanceof RegExp ||
      (typeof (renderer as RendererConfig).test === 'function' &&
        ((renderer as RendererConfig).test as Function).length < 2))
  ) {
    cache[path] = renderer;
  }

  return renderer;
}

export function getRenderers() {
  return renderers.concat();
}

export function getRendererByName(name: string) {
  return find(renderers, item => item.name === name);
}

setRenderSchemaFn((controls, value, callback, scopeRef, theme) => {
  return render(
    {
      name: 'form',
      type: 'form',
      wrapWithPanel: false,
      mode: 'horizontal',
      controls,
      messages: {
        validateFailed: ''
      }
    },
    {
      data: value,
      onFinished: callback,
      scopeRef,
      theme
    },
    {
      session: 'prompt'
    }
  );
});

export {RendererEnv};
