<script>
// #ifdef WEB || MP
import * as echarts from './js/echarts.esm.min.js';
import UniCanvas from './js/UniCanvas.js';
import UniImg from './js/UniImg.js';
import { getRandomId, addUnitRpx, getWindowInfo } from './js/util.js';
// #endif

// #ifdef APP
import WebviewEchart from './uts/WebviewEchart.uts';
import { getRandomId, addUnitRpx } from './uts/util.uts';
// #endif

/**
 * Echarts图表组件 (适配uni-app x)
 * @description 对Apache ECharts做uni-app的适配, 支持ECharts官方所有图表在各类小程序和APP使用
 * @property {Object} width 宽度 (支持格式: 600, "600rpx", "300px", "50vh", "100%", 其中数字默认rpx单位)
 * @property {Object} height 高度 (支持格式同width, 默认600rpx)
 * @property {Boolean} disableScroll 在图表区域内触摸移动时,是否禁止页面滚动 (默认false)
 * @property {String} theme 主题风格: 默认'light', 暗黑'dark' (非响应式,如需动态切换请参考示例)
 */
export default {
  name: 'e-chart',
  emits: ['ready'],
  props: {
    width: {
      type: [Number, String],
      default: '100%',
    },
    height: {
      type: [Number, String],
      default: 600,
    },
    disableScroll: Boolean,
    theme: String,
  },
  data() {
    return {
      // #ifdef APP
      echartObj: null as WebviewEchart | null,
      // #endif
      canvasId: getRandomId()
    };
  },
  computed: {
    canvasStyle() {
      return `width:${addUnitRpx(this.width)};height:${addUnitRpx(this.height)}`
    },
  },
  watch: {
    width(){
      this.resize()
    },
    height(){
      this.resize()
    }
  },
  mounted() {
    // 支付宝小程序需在onReady回调函数中获取canvas实例 https://opendocs.alipay.com/mini/component/canvas?pathHash=d0b85da4
    // #ifndef MP-ALIPAY
    this.onCanvasReady();
    // #endif
  },
  methods: {
    // 通知外部可初始化echarts实例 (vue3抖音小程序在页面的onMounted中echartRef.value没值, 而在ready回调中则有值)
    onCanvasReady() {
      this.$emit('ready')
    },

    // 外部通过ref初始化, 因为uni-app不支持props传递function属性
    async init(option: UTSJSONObject) {
      // #ifdef WEB
      return this.initWeb(option);
      // #endif

      // #ifdef MP
      return await this.initMP(option);
      // #endif

      // #ifdef APP
      return this.initAPP(option) as UTSJSONObject;
      // #endif
    },

    // #ifdef WEB
    initWeb(option: UTSJSONObject) {
      if (echarts.setPlatformAPI) {
        echarts.setPlatformAPI({
          loadImage: (src, onload, onerror) => {
            const image: any = new Image();

            image.crossOrigin = 'anonymous'; // 解决图表保存为图片时的跨域错误
            image.onload = onload;
            image.onerror = onerror;
            image.src = src;
            return image;
          },
        });
      }

      const echartDom = document.getElementById(this.canvasId);
      
      this.echartObj = echarts.init(echartDom, this.theme); 
      
      this.setOption(option);
      
      this.echartCanvas = echartDom.getElementsByTagName('canvas')[0];  // 必须先setOption,再获取canvas,否则是空白的canvas

      return { echartCanvas: this.echartCanvas, echartObj: this.echartObj, canvasId: this.canvasId, width: echartDom.clientWidth, height: echartDom.clientHeight }
    },
    // #endif

    // #ifdef MP
    async initMP(option: UTSJSONObject) {
      // node有值则使用的是canvas 2d, 可以提升渲染性能，解决非同层渲染问题; 否则是旧版canvas
      const { node, ctx, width, height, devicePixelRatio } = await this.getMpCanvas();

      const canvas = node ? new UniCanvas(ctx, this.canvasId, true, node) : new UniCanvas(ctx, this.canvasId, false);
      
      // 禁用progressive (非Web不支持DOM的操作)
      echarts.registerPreprocessor((option) => {
        if (option && option.series) {
          if (option.series.length > 0) {
            option.series.forEach((series) => {
              series.progressive = 0;
            });
          } else if (typeof option.series === 'object') {
            option.series.progressive = 0;
          }
        }
      });
  
      if (echarts.setPlatformAPI) {
        // 图表中的图片需使用canvas的createImage方法加载
        echarts.setPlatformAPI({
          createCanvas: () => canvas,
          loadImage: (src, onload, onerror) => {
            const image = node ? node.createImage() : new UniImg();
            image.onload = onload;
            image.onerror = onerror;
            image.src = src;
            return image;
          },
        });
      } else {
        // 低版本echarts
        echarts.setCanvasCreator(() => canvas);
      }

      // 初始化图表实例 (实例不写在vue的data, 无需响应式: https://echarts.apache.org/zh/faq.html#others)
      this.echartObj = echarts.init(canvas, this.theme, {
        devicePixelRatio,
        width,
        height,
      });

      canvas.setChart(this.echartObj);

      this.setOption(option);

      this.echartCanvas = node; // 缓存canvas节点,确保转存图片的时候能够取到(抖音小程序真机如果重新查询节点会转成失败,需事先缓存)

      return { echartCanvas: node, echartObj: this.echartObj, canvasId: this.canvasId, width, height }
    },
    // #endif

    // #ifdef APP
    initAPP(option: UTSJSONObject){
      const id = this.canvasId as string;
      const ctx = uni.createWebviewContext(id, this);
      if(ctx == null){
          console.error('初始化echart的webview失败')
          return null;
      }
      this.echartObj = new WebviewEchart(ctx);
	  this.echartObj.init(this.theme);
      this.setOption(option);
      return { echartObj: this.echartObj }
    },
    // #endif

    // 获取echart实例
    getEchartObj() {
      // #ifdef APP
      return this.echartObj as WebviewEchart;
      // #endif
	  // #ifndef APP
	  return this.echartObj 
	  // #endif
    },
	
    // 设置配置
    setOption(option: UTSJSONObject) {
      this.echartObj?.setOption(option);
    },
	
    // 获取配置
    getOption() {
		return new Promise<UTSJSONObject>(resolve=>{
			// #ifdef APP
			this.echartObj!.getOption(resolve);
			// #endif
			
			// #ifndef APP
			resolve(this.echartObj!.getOption())
			// #endif
			
		})
    },
	
    // 获取宽度
    getWidth() {
		return new Promise<number>(resolve=>{
			// #ifdef APP
			this.echartObj!.getWidth((e: UTSJSONObject)=>{
				resolve(e['width'] as number)
			});
			// #endif
			
			// #ifndef APP
			resolve(this.echartObj!.getWidth())
			// #endif
			
		})
    },
	
    // 获取宽度
    getHeight() {
		return new Promise<number>(resolve=>{
			// #ifdef APP
			this.echartObj!.getHeight((e: UTSJSONObject)=>{
				resolve(e['height'] as number)
			});
			// #endif
			
			// #ifndef APP
			resolve(this.echartObj!.getHeight())
			// #endif
			
		})
    },
	
    // 宽高变化需重绘 (使用setTimeout,避免$nextTick在某些机型不触发的问题)
    resize() {
      // #ifdef APP
      this.echartObj?.resize();
      // #endif
      
      // #ifndef APP
      this._resizeTimer && clearTimeout(this._resizeTimer);
      this._resizeTimer = setTimeout(() => {
        this.getCanvasQuery().boundingClientRect().exec(res => {
          const { width, height } = res[0];
          this.echartObj.resize({width, height}) // 必须设置width, height否则旧版canvas重绘失败
        });
      }, 30)
      // #endif
    },

    // #ifdef MP
    // 获取canvas查询对象
    getCanvasQuery() {
      let query = uni.createSelectorQuery();
      // #ifndef MP-ALIPAY
      query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
      // #endif
      return query.select(`#${this.canvasId}`)
    },
    
    // 获取Canvas上下文
    getMpCanvas() {
      return new Promise((resolve) => {
        this.getCanvasQuery().fields({ node: true, size: true }).exec((res) => {
          const { width, height, node } = res[0];
      
          let devicePixelRatio = 1; // 旧版canvas不支持dpr (新版2d支持, 使图表更清晰)
      
          if (node && node.getContext) {
            const ctxV2 = node.getContext('2d'); // 新版canvas (type="2d")
      
            // #ifndef MP-JD
            devicePixelRatio = getWindowInfo().pixelRatio; // 京东小程序仍使用1, 否则会被放大
            // #endif
      
            resolve({ ctx: ctxV2, width, height, node, devicePixelRatio });
          } else {
            const ctxV1 = uni.createCanvasContext(this.canvasId, this); // 旧版canvas (type="webgl")
            
            // #ifdef MP-LARK
            devicePixelRatio = getWindowInfo().pixelRatio; // 飞书小程序需使用真实dpr, 否则会被缩小
            // #endif
      
            resolve({ ctx: ctxV1, width, height, devicePixelRatio });
          }
        });
      });
    },
    // #endif

    // #ifdef WEB || MP
    // 触摸事件开始 - 将touch事件转为底层的mouse事件, 使点击或滑动相关事件生效 (解决移动端tooltip失效的问题)
    onTouchstart(e: UTSJSONObject) {
      const event = this.wrapTouch(e);
      const zrxy = this.getZrXy(event.touches);
      const handler = this.echartObj.getZr().handler;
    
      handler.dispatch('mousedown', zrxy);
      handler.dispatch('mousemove', zrxy); // 移动端tooltip需要move才显示, 加上这行确保点一下也能显示tooltip
      handler.processGesture(event, 'start');
    },
    
    // 触摸事件滑动
    onTouchmove(e: UTSJSONObject) {
      const event = this.wrapTouch(e);
      const zrxy = this.getZrXy(event.touches);
      const handler = this.echartObj.getZr().handler;
    
      handler.dispatch('mousemove', zrxy);
      handler.processGesture(event, 'change');
    },
    
    // 触摸事件结束
    onTouchend(e: UTSJSONObject) {
      const event = this.wrapTouch(e);
      const zrxy = this.getZrXy(event.changedTouches);
      const handler = this.echartObj.getZr().handler;
    
      handler.dispatch('mouseup', zrxy);
      handler.dispatch('click', zrxy);
      handler.processGesture(event, 'end');
    },
    
    // touches补充offsetX和offsetY
    wrapTouch(e: UTSJSONObject) {
      this.wrapTouches(e.changedTouches)
      this.wrapTouches(e.touches)
      return e;
    },
    
    wrapTouches(touches: UTSJSONObject[] = []) { 
      for (const touch of touches) {
    
        // #ifdef WEB
        // h5只能用普通div,无法使用canvas,否则初始化echart报错,此处需补充canvas专有的touch.x和touch.y
        const rect = this.echartCanvas.getBoundingClientRect();
        if(rect && touch.clientX != null && touch.clientY != null){
            touch.x = touch.clientX - rect.left;
            touch.y = touch.clientY - rect.top;
        }
        // #endif
    
        touch.offsetX = touch.x;
        touch.offsetY = touch.y;
      }
    },
    
    // 转换触摸事件的坐标 (touchend和touchcancel使用changedTouches, 而touchstart和touchmove使用touches)
    getZrXy(touches: UTSJSONObject[] = []): UTSJSONObject {
      const touch = touches[0] || {};
    
      return {
        zrX: touch.x,
        zrY: touch.y,
        preventDefault: () => {},
        stopImmediatePropagation: () => {},
        stopPropagation: () => {}
      };
    },
    // #endif

    // canvas转为文件路径
    canvasToTempFilePath() {
      return new Promise<string>((resolve, reject) => {
        const fail = (e: UTSJSONObject) => {
          uni.showModal({ title: '保存失败', content: JSON.stringify(e), showCancel: false });
          reject(e);
        };

        // #ifdef WEB
        // Web端转base64直接下载
        resolve(this.echartCanvas.toDataURL('image/png'));
        // #endif

        // #ifdef MP-TOUTIAO
        // 抖音小程序: https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/canvas/v1/canvas-to-temp-file-path
        const base64 = this.echartCanvas.toDataURL(); 
        const fileSystemManager = tt.getFileSystemManager()
        const filePath = `ttfile://user/${Date.now()}.png`;
        fileSystemManager.writeFile({
          filePath,
          encoding: "base64",
          data: base64.replace('data:image/png;base64,', ''),
          success: () => {
            resolve(filePath);
          },
          fail
        })
        // #endif

        // #ifndef APP || WEB || MP-TOUTIAO
        // 小程序 (非抖音)
        const option = this.echartCanvas ? { canvas: this.echartCanvas } : { canvasId: this.canvasId }
        uni.canvasToTempFilePath({
          ...option,
          success(res){
            resolve(res.tempFilePath)
          },
          fail
        });
        // #endif
        
        // #ifdef APP
        this.echartObj?.canvasToTempFilePath({
            success: (e: UTSJSONObject) => {
                const base64 = e['base64'] as string;
                const fileSystemManager = uni.getFileSystemManager()
                const filePath = `${uni.env.USER_DATA_PATH}/${Date.now()}.png`;
                fileSystemManager.writeFile({
                  filePath,
                  encoding: "base64",
                  data: base64.replace('data:image/png;base64,', ''),
                  success: () => {
                    resolve(filePath);
                  },
				  fail
                })
            }, 
            fail
        });
        // #endif
      });
    },
	
	// 显示进度条
	showLoading(option: UTSJSONObject){
	  this.echartObj?.showLoading(option)
	},
	
	// 隐藏进度条
	hideLoading(){
	  this.echartObj?.hideLoading()
	},

   // #ifdef APP
   // webview消息
   onWebviewMsg(e: UniWebViewMessageEvent){
     this.echartObj?.onWebviewMsg(e)
   },
   // webview加载失败的回调
   onWebviewError(e: UniWebViewErrorEvent){
     console.error('onWebviewError:', e)
   }
   // #endif
  },
};
</script>

<template>
  <!-- #ifdef WEB -->
  <div 
    :id="canvasId" 
    :style="canvasStyle" 
    @touchstart="onTouchstart"
    @touchmove="onTouchmove"
    @touchend="onTouchend"
    @touchcancel="onTouchend"
  />
  <!-- #endif -->
  
  <!-- #ifdef MP -->
  <canvas
    v-if="canvasId"
    type="2d"
    :id="canvasId"
    :style="canvasStyle"
    :canvas-id="canvasId"
    :disable-scroll="disableScroll"
    @touchstart="onTouchstart"
    @touchmove="onTouchmove"
    @touchend="onTouchend"
    @touchcancel="onTouchend"
    @ready="onCanvasReady"
  />
  <!-- #endif -->

  <!-- #ifdef APP -->
  <web-view 
    :id="canvasId"
    :style="canvasStyle"
    :bounces="false"
    :horizontalScrollBarAccess="false"
    :verticalScrollBarAccess="false"
    :webview-styles="{progress: false}"
    src="/uni_modules/e-chart/static/app/webview.html"
    @load="onCanvasReady"
    @error="onWebviewError"
    @message="onWebviewMsg"
   />
  <!-- #endif -->
</template>