import React from 'react';
import { Renderer, RendererProps } from '../factory';
import { ServiceStore, IServiceStore } from '../store/service';
import { filter } from '../utils/tpl';
import cx from 'classnames';
import LazyComponent from '../components/LazyComponent';
import { resizeSensor } from '../utils/resize-sensor';
import {
  resolveVariableAndFilter,
  isPureVariable,
  dataMapping
} from '../utils/tpl-builtin';
import {
  isApiOutdated,
  isEffectiveApi,
  normalizeApiResponseData
} from '../utils/api';
import { ScopedContext, IScopedContext } from '../Scoped';
import { createObject, findObjectsWithKey, isMobile } from '../utils/helper';
import {
  BaseSchema,
  SchemaApi,
  SchemaExpression,
  SchemaFunction,
  SchemaName,
  SchemaTokenizeableString
} from '../Schema';
import { ActionSchema } from './Action';
import { isAlive } from 'mobx-state-tree';
// Jay
import Empty from 'antd/lib/empty';
import { linkJump, ModleHandleClick } from '../utils/utils';
import { Action } from '../types';

/**
 * Chart 图表渲染器。
 * 文档：https://baidu.gitee.io/amis/docs/components/carousel
 */
export interface ChartSchema extends BaseSchema {
  /**
   * 指定为 chart 类型
   */
  type: 'chart';

  /**
   * Chart 主题配置
   */
  chartTheme?: any;

  /**
   * 图表配置接口
   */
  api?: SchemaApi;

  /**
   * 是否初始加载。
   * @deprecated 建议直接配置 api 的 sendOn
   */
  initFetch?: boolean;

  /**
   * 是否初始加载用表达式来配置
   * @deprecated 建议用 api.sendOn 属性。
   */
  initFetchOn?: SchemaExpression;

  /**
   * 配置echart的config，支持数据映射。如果用了数据映射，为了同步更新，请设置 trackExpression
   */
  config?: any;

  /**
   * 跟踪表达式，如果这个表达式的运行结果发生变化了，则会更新 Echart，当 config 中用了数据映射时有用。
   */
  trackExpression?: string;

  /**
   * 宽度设置
   */
  width?: number;

  /**
   * 高度设置
   */
  height?: number;

  /**
   * 刷新时间
   */
  interval?: number;

  name?: SchemaName;

  /**
   * style样式
   */
  style?: {
    [propName: string]: any;
  };

  dataFilter?: SchemaFunction;

  source?: SchemaTokenizeableString;

  /**
   * 默认开启 Config 中的数据映射，如果想关闭，请开启此功能。
   */
  disableDataMapping?: boolean;

  /**
   * 点击行为配置，可以用来满足下钻操作等。
   */
  clickAction?: ActionSchema & {
    linkId?: string;
    linkUrl?: string;
    linkTitle?: string;
    linkType?: string;
    type?: string;
  };

  /**
   * 默认配置时追加的，如果更新配置想完全替换配置请配置为 true.
   */
  replaceChartOption?: boolean;

  /**
   * 不可见的时候隐藏
   */
  unMountOnHidden?: boolean;
  /**
   * 蒙版图层
   */
  maskImageUrl?: string;
}

const EVAL_CACHE: { [key: string]: Function } = {};
/**
 * ECharts 中有些配置项可以写函数，但 JSON 中无法支持，为了实现这个功能，需要将看起来像函数的字符串转成函数类型
 * 目前 ECharts 中可能有函数的配置项有如下：interval、formatter、color、min、max、labelFormatter、pageFormatter、optionToContent、contentToOption、animationDelay、animationDurationUpdate、animationDelayUpdate、animationDuration、position、sort
 * 其中用得最多的是 formatter、sort，所以目前先只支持它们
 * @param config ECharts 配置
 */
function recoverFunctionType(config: object) {
  ['formatter', 'sort', 'renderItem', 'symbolSize', 'valueFormatter'].forEach((key: string) => {
    const objects = findObjectsWithKey(config, key);
    for (const object of objects) {
      const code = object[key];
      if (typeof code === 'string' && code.trim().startsWith('function')) {
        try {
          if (!(code in EVAL_CACHE)) {
            EVAL_CACHE[code] = eval('(' + code + ')');
          }
          object[key] = EVAL_CACHE[code];
        } catch (e) {
          console.warn(code, e);
        }
      }
    }
  });
}

export interface ChartProps
  extends RendererProps,
  Omit<ChartSchema, 'type' | 'className'> {
  chartRef?: (echart: any) => void;
  onDataFilter?: (config: any, echarts: any, data?: any) => any;
  onChartWillMount?: (echarts: any) => void | Promise<void>;
  onChartMount?: (chart: any, echarts: any) => void;
  onChartUnMount?: (chart: any, echarts: any) => void;
  store: IServiceStore;
}

// Jay
interface ChartState {
  hasRender: boolean;
  canvasHigh: string;
  visible: boolean;
}
export class Chart extends React.Component<ChartProps, ChartState> {
  static defaultProps: Partial<ChartProps> = {
    replaceChartOption: false,
    unMountOnHidden: false
  };

  static propsList: Array<string> = [];

  ref: any;
  echarts?: any;
  unSensor: Function;
  pending?: object;
  pendingCtx?: any;
  timer: ReturnType<typeof setTimeout>;
  mounted: boolean;
  reloadCancel?: Function;

  constructor(props: ChartProps) {
    super(props);

    this.refFn = this.refFn.bind(this);
    this.reload = this.reload.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleExport = this.handleExport.bind(this)
    this.handleDialogConfirm = this.handleDialogConfirm.bind(this);
    this.handleDrawerConfirm = this.handleDrawerConfirm.bind(this);
    this.baseModalConfirm = this.baseModalConfirm.bind(this);
    this.handleDialogClose = this.handleDialogClose.bind(this);
    this.handleDrawerClose = this.handleDrawerClose.bind(this);
    this.mounted = true;

    props.config && this.renderChart(props.config);

    // Jay
    this.state = {
      hasRender: false,
      canvasHigh: '100%',
      visible: false
    }
  }

  componentDidMount() {
    const { api, data, initFetch, source } = this.props;
    if (source && isPureVariable(source)) {
      const ret = resolveVariableAndFilter(source, data, '| raw');
      ret && this.renderChart(ret);
    } else if (api && initFetch !== false) {
      // Jay
      this.props.setPanelLoading?.(true)

      this.reload();
    }
  }

  componentDidUpdate(prevProps: ChartProps) {
    const props = this.props;

    if (isApiOutdated(prevProps.api, props.api, prevProps.data, props.data)) {
      this.reload();
    } else if (props.source && isPureVariable(props.source)) {
      const prevRet = prevProps.source
        ? resolveVariableAndFilter(prevProps.source, prevProps.data, '| raw')
        : null;
      const ret = resolveVariableAndFilter(props.source, props.data, '| raw');

      if (prevRet !== ret) {
        this.renderChart(ret || {});
      }
    } else if (props.config !== prevProps.config) {
      this.renderChart(props.config || {});
    } else if (
      props.config &&
      props.trackExpression &&
      filter(props.trackExpression, props.data) !==
      filter(prevProps.trackExpression, prevProps.data)
    ) {
      this.renderChart(props.config || {});
    }
  }

  componentWillUnmount() {
    this.mounted = false;
    clearTimeout(this.timer);
  }

  async handleExport({ api }: { api: SchemaApi }) {
    const res = await this.props.env.fetcher(api)
    if (res.ok && res.data != null) {
      const baseUrl = (this.props.env?.axiosInstance?.defaults?.baseURL ?? res?.reqUrl )?? ''
      const link = document.createElement('a')
      link.href = baseUrl + res.data.fileUrl
      link.download = res.data.fileName
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    } 
  }

  handleClick(ctx: obj) {
    const { onAction, clickAction, data } = this.props;
    clickAction &&
      onAction &&
      onAction(null, clickAction, createObject(data, ctx.data));
    if(clickAction?.linkId) {
      const dataObj = createObject(data, this.dataset[ctx.dataIndex]);
      linkJump(clickAction?.linkId ?? '', dataObj) && ModleHandleClick({ ...this.props, ...clickAction, value: ctx.value, data: dataObj, handleJump: this.handleJump })
    }
  }

  handleJump = (body: any) => {
    const { store } = this.props;
    const types = isMobile() ? 'drawer' : 'dialog';
    const action: any = {
      type: "action",
      actionType: types,
      close: true
    }
    action[types] = {
      title: body.linkTitle,
      type: types,
      size: body.linkSize ?? 'lg',
      bodyClassName: `overflow-y-auto max-h-nestSide-${types}`,
      className: "h-full",
      actions: [],
      overlay: isMobile() ? false : true,
      body: {
        schemaApi: {
          "method": "get",
          "url": body.linkUrl
        },
        type: "service"
      }
    }
    store.setCurrentAction(action);
    types === 'dialog' ? store.openDialog(body.bodydata) : store.openDrawer(body.bodydata)
  }

  chartContent: any

  refFn = async (ref: any) => {
    this.chartContent = ref // 记录表格体
    const chartRef = this.props.chartRef;
    const { chartTheme, onChartWillMount, onChartUnMount, env } = this.props;
    let onChartMount = this.props.onChartMount;

    if (ref) {
      // Promise.all([
      //   import('echarts'),
      //   import('echarts-stat'),
      //   import('echarts/extension/dataTool'),
      //   import('echarts/extension/bmap/bmap')
      // ]).then(async ([echarts, ecStat]) => {
      //   (window as any).echarts = echarts;
      //   (window as any).ecStat = ecStat;
      let theme = 'default';

      if (chartTheme) {
        echarts.registerTheme('custom', chartTheme);
        theme = 'custom';
      }

      if (onChartWillMount) {
        await onChartWillMount(echarts);
      }

      // (echarts as any).registerTransform(
      //   (ecStat as any).transform.regression
      // );
      // (echarts as any).registerTransform((ecStat as any).transform.histogram);
      // (echarts as any).registerTransform(
      //   (ecStat as any).transform.clustering
      // );

      if (env.loadChartExtends) {
        await env.loadChartExtends();
      }
      this.echarts = (echarts as any).init(ref, theme);
      if (typeof onChartMount === 'string') {
        onChartMount = new Function('chart', 'echarts') as any;
      }

      onChartMount?.(this.echarts, echarts);
      this.unSensor = resizeSensor(ref, () => {
        const width = ref.offsetWidth;
        const height = ref.offsetHeight;
        this.echarts?.resize({
          width,
          height
        });
      });

      chartRef && chartRef(this.echarts);
      this.renderChart();
      // });
    } else {
      chartRef && chartRef(null);
      this.unSensor && this.unSensor();

      if (this.echarts) {
        onChartUnMount?.(this.echarts, (window as any).echarts);
        this.echarts.dispose();
        delete this.echarts;
      }
    }

    this.ref = ref;
  }

  reload(subpath?: string, query?: any) {
    const { api, env, store, interval, translate: __ } = this.props;

    if (query) {
      return this.receive(query);
    } else if (!env || !env.fetcher || !isEffectiveApi(api, store.data)) {
      return;
    }

    clearTimeout(this.timer);
    if (this.reloadCancel) {
      this.reloadCancel();
      delete this.reloadCancel;
      this.echarts?.hideLoading();
    }
    this.echarts?.showLoading();

    store.markFetching(true);
    env
      .fetcher(api, store.data, {
        cancelExecutor: (executor: Function) => (this.reloadCancel = executor)
      })
      .then(result => {
        this.props.setPanelLoading?.(false) // Jay
        isAlive(store) && store.markFetching(false);

        if (!result.ok) {
          this.renderChart({});
          return env.notify(
            'error',
            result.msg || __('fetchFailed'),
            result.msgTimeout !== undefined
              ? {
                closeButton: true,
                timeout: result.msgTimeout
              }
              : undefined
          );
        }
        delete this.reloadCancel;

        const data = normalizeApiResponseData(result.data);

        // 说明返回的是数据接口。
        if (!data.series && this.props.config) {
          const ctx = createObject(this.props.data, data);
          this.renderChart(this.props.config, ctx);
        } else {
          this.renderChart(result.data || {});
        }

        this.echarts?.hideLoading();

        interval &&
          this.mounted &&
          (this.timer = setTimeout(this.reload, Math.max(interval, 1000)));
      })
      .catch(reason => {
        this.props.setPanelLoading?.(false) // Jay
        if (env.isCancel(reason)) {
          return;
        }

        isAlive(store) && store.markFetching(false);
        env.notify('error', reason);
        this.echarts?.hideLoading();
      });
  }

  receive(data: object) {
    const store = this.props.store;

    store.updateData(data);
    this.reload();
  }

  dataset: obj[] = [];

  renderChart(config?: any, data?: any) {
    config && (this.pending = config);
    data && (this.pendingCtx = data);

    if (!this.echarts) {
      return;
    }

    const store = this.props.store;
    let onDataFilter = this.props.onDataFilter;
    const dataFilter = this.props.dataFilter;

    if (!onDataFilter && typeof dataFilter === 'string') {
      onDataFilter = new Function(
        'config',
        'echarts',
        'data',
        dataFilter
      ) as any;
    }

    config = config || this.pending;
    data = data || this.pendingCtx || this.props.data;

    if (!config?.dataset) {
      config = {
        ...config,
        dataset: { source: [] }
      }
    }
    if (typeof config === 'string') {
      config = new Function('return ' + config)();
    }
    try {
      onDataFilter &&
        (config =
          onDataFilter(config, (window as any).echarts, data) || config);
    } catch (e) {
      console.warn(e);
    }

    if (config) {
      try {
        if (!this.props.disableDataMapping) {
          config = dataMapping(
            config,
            data,
            (key: string, value: any) =>
              typeof value === 'function' ||
              (typeof value === 'string' && value.startsWith('function'))
          );
        }
        if (config.toolbox?.feature?.myExport) {
          config.toolbox.feature.myExport.icon = `path://M997.910386 1023.826532H25.812065C11.622368 1023.826532 0 1012.204164 0 997.99712V25.933493C0 11.726449 11.622368 0.104081 25.812065 0.104081c14.207044 0 25.829412 11.622368 25.829412 25.829412v946.234215h946.268909c14.207044 0 25.829412 11.622368 25.829412 25.829412 0 14.207044-11.622368 25.829412-25.829412 25.829412z m-117.52469-448.050956v292.710203c0 14.207044-11.622368 25.829412-25.829412 25.829411-14.189697 0-25.812065-11.622368-25.812065-25.829411V575.775576c0-14.207044 11.622368-25.829412 25.812065-25.829412 14.207044 0 25.829412 11.622368 25.829412 25.829412zM650.054649 463.853907c14.207044 0 25.812065 11.622368 25.812065 25.829412v378.80246c0 14.207044-11.605021 25.829412-25.812065 25.829411-14.189697 0-25.812065-11.622368-25.812065-25.829411V489.683319c0-14.207044 11.622368-25.829412 25.812065-25.829412z m-182.766072-77.470888v482.10276c0 14.207044-11.622368 25.829412-25.812065 25.829411-14.207044 0-25.829412-11.622368-25.829411-25.829411V386.383019c0-14.207044 11.622368-25.829412 25.829411-25.829412 14.189697 0 25.812065 11.622368 25.812065 25.829412z m-232.360624 163.563145c14.189697 0 25.812065 11.622368 25.812065 25.829412v292.710203c0 14.207044-11.622368 25.829412-25.812065 25.829411-14.207044 0-25.812065-11.622368-25.812065-25.829411V575.775576c0-14.207044 11.605021-25.829412 25.812065-25.829412z m418.405245-290.229608c-6.86934 5.585675-15.681523 6.557097-23.609019 4.19793-4.458132 0.884688-9.072386 1.040809-13.634599-0.607139l-207.589368-75.077027-162.192746 93.083024a25.864105 25.864105 0 0 1-34.554861-11.761143 25.881452 25.881452 0 0 1 11.761142-34.572207l171.941658-98.686047c7.545866-3.729566 15.907032-2.983653 22.915146 0.589792l214.424015 77.540276 226.54944-97.95748a25.898799 25.898799 0 0 1 36.324236 3.781606 25.916146 25.916146 0 0 1-3.764259 36.324236l-238.570785 103.144179z`
          config.toolbox.feature.myExport.onclick = () => { this.handleExport(config.toolbox.feature.myExport) }
        }
        const chartObj = config.series?.[0]
        if(chartObj) {
          this.dataset = chartObj.type === 'scatter' ? chartObj.data.map((item: any[]) => {
            const jumpData = item[item.length - 1];
            if(typeof jumpData == 'object') return jumpData;
            return {};
          }) : (chartObj.data?.length > 0 ? chartObj.data : config.dataset.source);
        }
        recoverFunctionType(config!);

        if (isAlive(store) && store.loading) {
          this.echarts?.showLoading();
        } else {
          this.echarts?.hideLoading();
        }
        this.setState({ hasRender: true }) // Jay
        // 强制重绘
        this.echarts.clear();
        if (this.props.maskImageUrl) {
          const maskImage = new Image();
          maskImage.src = this.props.maskImageUrl;
          config = {
            ...config,
            maskImage
          }
        }
        this.echarts?.setOption(config!, this.props.replaceChartOption);
        this.echarts.on('click', this.handleClick);
      } catch (e) {
        console.warn(e);
      }
    }
  }

  async handleDialogConfirm(
    values: object[],
    action: Action,
    ctx: any,
    components: Array<any>,
  ) {
    return await this.baseModalConfirm('dialog')(values, action, ctx,
      components)
  }

  async handleDrawerConfirm(
    values: object[],
    action: Action,
    ctx: any,
    components: Array<any>,
  ) {
    return await this.baseModalConfirm('drawer')(values, action, ctx,
      components)
  }

  baseModalConfirm(type: 'drawer' | 'dialog') {
    return async (values: object[],
      action: Action,
      ctx: any,
      components: Array<any>) => {
      const {
        store,
        pageField,
        stopAutoRefreshWhenModalIsOpen,
        interval,
        silentPolling,
        env
      } = this.props;
      switch (type) {
        case 'drawer':
        default:
          store.closeDrawer(true);
          break
        case 'dialog':
          store.closeDialog(true);
          break;
      }
      // const dialogAction = store.action as Action;
      // const reload = action.reload ?? dialogAction.reload;
      // if (reload) {
      //   this.reloadTarget(reload, ctx);
      // }
      // let redirect = action.redirect ?? dialogAction.redirect;
      // redirect = redirect && filter(redirect, ctx);
      // redirect && env.jumpTo(redirect, dialogAction);
    }
  }
  handleDialogClose(
    confirmed?: boolean,
    formInstance?: any,
  ) {
    return this.handleBaseModalClose('dialog')(confirmed, formInstance)
  }

  handleDrawerClose(
    confirmed?: boolean,
    formInstance?: any,
  ) {
    return this.handleBaseModalClose('drawer')(confirmed, formInstance)
  }


  handleBaseModalClose(type: 'dialog' | 'drawer') {
    return (confirmed?: boolean, formInstance?: any) => {
      const { translate: __, store } = this.props;
      switch (type) {
        case 'dialog':
          store.closeDialog();
          break;
        case 'drawer':
          store.closeDrawer();
          break;
      }
    }
  }

  render() {
    const {
      className,
      width,
      height,
      classPrefix: ns,
      unMountOnHidden,
      render,
      store
    } = this.props;
    let style = this.props.style || {};

    width && (style.width = width);
    height && (style.height = height);

    return (
      <div className={cx(`${ns}Chart`, className)} style={style}>
        <LazyComponent
          unMountOnHidden={unMountOnHidden}
          placeholder="..." // 之前那个 spinner 会导致 sensor 失效
          component={() => (
            <>
              <div className={`${ns}Chart-content`} ref={this.refFn} style={{ height: height, width }}></div>
              {/* Jay */}
              {(!this.state.hasRender) &&
                <div style={{
                  display: 'flex', justifyContent: 'center',
                  alignItems: 'center', width: '100%', height: '100%', minHeight: 300,
                }}>
                  <Empty description={this.props.translate(this.props.placeholder || 'placeholder.noData')} />
                </div>}
            </>
          )}
        />
        {
          this.props.store.dialogOpen ?
            render(
              'dialog',
              {
                ...((store.action as Action) &&
                  ((store.action as Action).dialog as object)),
                type: 'dialog'
              },
              {
                key: 'dialog',
                data: store.dialogData,
                onConfirm: this.handleDialogConfirm,
                onClose: this.handleDialogClose,
                show: store.dialogOpen,
                replace: store.replace
              }
            ) : null
        }
        {
          this.props.store.drawerOpen ?
            render(
              'drawer',
              {
                ...((store.action as Action) &&
                  ((store.action as Action).drawer as object)),
                type: 'drawer'
              },
              {
                key: 'drawer',
                data: store.drawerData,
                onConfirm: this.handleDrawerConfirm,
                onClose: this.handleDrawerClose,
                show: store.drawerOpen,
                // onAction: this.handleAction,
              }
            ) : null
        }
      </div>
    );
  }
}

@Renderer({
  type: 'chart',
  storeType: ServiceStore.name
})
export class ChartRenderer extends Chart {
  static contextType = ScopedContext;

  constructor(props: ChartProps, context: IScopedContext) {
    super(props);

    const scoped = context;
    scoped.registerComponent(this);
  }

  componentWillUnmount() {
    super.componentWillUnmount();
    const scoped = this.context as IScopedContext;
    scoped.unRegisterComponent(this);
  }
}
