import React from 'react';
import { Renderer, RendererEnv, RendererProps } from '../factory';
import { Api, Payload } from '../types';
import {
  BaseSchema,
  SchemaApi,
  SchemaTokenizeableString,
  SchemaTpl
} from '../Schema';
import { withStore } from '../components/WithStore';
import { flow, Instance, types } from 'mobx-state-tree';
import { getPropValue, guid, isObject } from '../utils/helper';
import { StoreNode } from '../store/node';
import { filters, isPureVariable, resolveVariableAndFilter } from '../utils/tpl-builtin';
import {
  isApiOutdated,
  isEffectiveApi,
  normalizeApi,
  normalizeApiResponseData
} from '../utils/api';
import { filter } from '../utils/tpl';
import { EventEnum, EventSub } from '../utils/sub';
import { IColumn } from '../store/table';

/**
 * Mapping 映射展示控件。
 * 文档：https://baidu.gitee.io/amis/docs/components/mapping
 */
export interface MappingSchema extends BaseSchema {
  /**
   * 指定为映射展示控件
   */
  type: 'map' | 'mapping';

  /**
   * 关联字段名。
   */
  name?: string;

  /**
   * 配置映射规则，值可以使用模板语法。当 key 为 * 时表示 else，也就是说值没有映射到任何规则时用 * 对应的值展示。
   */
  map?: {
    [propName: string]: SchemaTpl;
  };

  /**
   * 如果想远程拉取字典，请配置 source 为接口。
   */
  source?: SchemaApi | SchemaTokenizeableString;

  /**
   * 占位符
   */
  placeholder?: string;
}

// 缓存一下结果 省的每次同一个请求反复走调用流程
const MappingCache = {}
// 多个请求并发的时候只有一个可用
const WaittingCache = {}
// 注册监听 更新页面的时候情况缓存
EventSub.on(EventEnum.ClearMappingAcahe, (InstanceName) => {
  if (!InstanceName) return
  delete MappingCache[InstanceName]
})

export const Store = StoreNode.named('MappingStore')
  .props({
    fetching: false,
    errorMsg: '',
    map: types.frozen<{
      [propName: string]: any;
    }>({})
  })
  .actions(self => {
    const load: (env: RendererEnv, api: Api, data: any, crudName: string, crudColumn?: IColumn) => Promise<any> = flow(
      function* (env, api, data, crudName, crudColumn) {
        const fetchUrl = filter((api as { url: string })?.url || '', data) // 实际的请求地址
        try {
          if (!WaittingCache[fetchUrl]) {
            // 如果已经没有有在等待的请求队列 正常请求 请求完触发所有请求队列
            WaittingCache[fetchUrl] = []
            self.fetching = true;
            const ret: Payload = yield env.fetcher(api, data);

            if (ret.ok) {
              const returnData = normalizeApiResponseData(ret.data);
              let mapData = returnData
              // 如果返回空值不修改结果，直接使用通配值
              if (returnData?.result === null && Object.keys(returnData).length === 1) {
                mapData = { '*': '通配值' }
              }
              // 防止出现空map的情况
              MappingCache[crudName][fetchUrl] = { ...mapData };
              crudColumn?.setMapValue?.(returnData)

              // 触发所有等待的请求
              WaittingCache[fetchUrl].map((wattingFn: any) => {
                wattingFn?.()
              })
              // 用完就可以删除掉了
              delete WaittingCache[fetchUrl];
              (self as any).setMap(mapData);
            } else {
              throw new Error(ret.msg || 'fetch error');
            }
          } else {
            // 如果已经有在等待的请求队列 推入等待列表
            WaittingCache[fetchUrl].push(() => {
              (self as any).setMap(MappingCache[crudName][fetchUrl]);
            })
          }
        } catch (e) {
          self.errorMsg = e.message;
        } finally {
          self.fetching = false;
        }
      }
    );

    return {
      load,
      setMap(options: any) {
        if (isObject(options)) {

          self.map = {
            ...options
          };
        }
      }
    };
  });

export type IStore = Instance<typeof Store>;

export interface MappingProps
  extends Omit<RendererProps, 'store'>,
  Omit<MappingSchema, 'type' | 'className'> {
  store: IStore;
  crudColumn?: IColumn
}

export const MappingField = withStore(props =>
  Store.create(
    {
      id: guid(),
      storeType: Store.name
    },
    props.env
  )
)(
  class extends React.Component<MappingProps, object> {
    static defaultProps: Partial<MappingProps> = {
      placeholder: '-',
      map: {
        '*': '通配值'
      }
    };

    constructor(props: MappingProps) {
      super(props);
      const { crudName } = this.props
      // 没有对应的缓存 初始化缓存
      if (!MappingCache[crudName]) {
        MappingCache[crudName] = {}
      }
      props.store.syncProps(props, undefined, ['map']);
    }

    componentDidMount() {
      const { store, source, data } = this.props;

      if (!this.props.formItem) {
        this.reload();
      }
    }

    privateFormInitedStatus = false;

    componentDidUpdate(prevProps: MappingProps) {
      const props = this.props;
      const { store, source, data } = this.props;

      store.syncProps(props, prevProps, ['map']);

      if (isPureVariable(source)) {
        const prev = resolveVariableAndFilter(
          prevProps.source as string,
          prevProps.data,
          '| raw'
        );
        const curr = resolveVariableAndFilter(source as string, data, '| raw');

        if (prev !== curr) {
          store.setMap(curr);
        }
      } else if (
        isApiOutdated(
          prevProps.source,
          props.source,
          prevProps.data,
          props.data
        )
      ) {
        this.reload();
      } else if (
        prevProps.source !== props.source
      ) {
        this.reload()
      } else if (!this.privateFormInitedStatus && this.props.formInited) {
        this.reload()
      }
      // 是有组件内部的状态来管理，防止出现pre的值域props的值相同产生无法更新的问题
      this.privateFormInitedStatus = this.props.formInited
    }

    reload() {
      const { source, data, env, crudName, crudColumn } = this.props;
      const store = this.props.store;
      if (isPureVariable(source)) {
        store.setMap(resolveVariableAndFilter(source, data, '| raw'));
      } else if (isEffectiveApi(source, data)) {
        // 没缓存走请求 有缓存直接设置
        const cacheData = MappingCache[crudName][filter(source.url, data)]
        if (!cacheData) {
          const api = normalizeApi(source, 'get');
          api.cache = api.cache ?? 60 * 1000;
          store.load(env, api, data, crudName, crudColumn);
        } else {
          store.setMap(cacheData)
          crudColumn?.setMapValue?.(cacheData)
        }
      }
    }

    renderSingleValue(key: any, reactKey?: number) {
      const {
        className,
        placeholder,
        render,
        classnames: cx,
        name,
        data,
        store
      } = this.props;
      let viewValue: React.ReactNode = (
        <span className="text-muted">{placeholder}</span>
      );
      const map = store.map;
      let value: any = undefined;
      // trim 一下，干掉一些空白字符。
      key = typeof key === 'string' ? key.trim() : key;
      if (
        typeof key !== 'undefined' &&
        map &&
        (value =
          map[key] ??
          (key === true && map['1']
            ? map['1']
            : key === false && map['0']
              ? map['0']
              : (key || map['*']))) !== undefined
      ) {
        viewValue = render('tpl', value);
      }
      return (
        <span key={`map-${reactKey}`} className={cx('MappingField', className)}>
          {viewValue}
        </span>
      );
    }

    render() {
      let mapKey = getPropValue(this.props);
      const { delimiter = ',', classnames: cx, store } = this.props
      const map = store.map;
      if (delimiter && typeof mapKey == 'string' && mapKey.indexOf(delimiter) !== -1) {
        mapKey = mapKey.split(delimiter)
      }
      if (typeof mapKey == 'string' && mapKey.indexOf(',') !== -1) {
        mapKey = mapKey.split(',')
      }

      if (Array.isArray(mapKey)) {
        const value = mapKey.map(item => map[item] && map[item]).filter(item => item)
        const mapValue = value.some(item => !item.indexOf('<span'));
        return (
          <span className={cx('Mapping')} title={mapValue ? "" : value.length ? value.join(delimiter) : ''}>
            {mapValue ?
              mapKey.map((singleKey: string, index: number) =>
                this.renderSingleValue(singleKey, index)
              )
              :
              value.length ? value.join(delimiter) : ''
            }
          </span>
        );
      } else {
        return this.renderSingleValue(mapKey, 0);
      }
    }
  }
);

@Renderer({
  test: /(^|\/)(?:map|mapping)$/,
  name: 'mapping'
})
export class MappingFieldRenderer extends React.Component<RendererProps> {
  render() {
    return <MappingField {...this.props} />;
  }
}
