import { ref, computed, ExtractPropTypes, Ref, onMounted } from 'vue'
import { extend } from '@vue/shared'
import type { Actions } from '@dcloudio/uni-api'
import {
  useAttrs,
  useContextInfo,
  useSubscribe,
  withWebEvent,
  defineBuiltInComponent,
} from '@dcloudio/uni-components'
import { onEventPrevent } from '@dcloudio/uni-core'
import {
  saveImage,
  getSameOriginUrl,
  getRealPath,
  inflateRaw,
  deflateRaw,
} from '@dcloudio/uni-platform'
import ResizeSensor from '../resize-sensor'
import { useNativeEvent, NativeEventTrigger } from '../../helpers/useEvent'
import { pixelRatio, wrapper, initHidpi } from '../../helpers/hidpi'
import { once } from '@dcloudio/uni-shared'

const initHidpiOnce = /*#__PURE__*/ once(() => {
  return __NODE_JS__ ? onMounted(initHidpi) : initHidpi()
})

function $getRealPath(src: string) {
  return src ? getRealPath(src) : src
}

function resolveColor(color: number[]) {
  color = color.slice(0)
  color[3] = color[3] / 255
  return 'rgba(' + color.join(',') + ')'
}

function processTouches(target: EventTarget, touches: TouchEvent['touches']) {
  const eventTarget = target as HTMLElement
  return Array.from(touches).map((touch) => {
    let boundingClientRect = eventTarget.getBoundingClientRect()
    return {
      identifier: touch.identifier,
      x: touch.clientX - boundingClientRect.left,
      y: touch.clientY - boundingClientRect.top,
    }
  })
}

let tempCanvas: HTMLCanvasElement
function getTempCanvas(width = 0, height = 0) {
  if (!tempCanvas) {
    tempCanvas = document.createElement('canvas')
  }
  tempCanvas.width = width
  tempCanvas.height = height
  return tempCanvas
}

const props = {
  canvasId: {
    type: String,
    default: '',
  },
  disableScroll: {
    type: [Boolean, String],
    default: false,
  },
  hidpi: {
    type: Boolean,
    default: true,
  },
}

type Props = ExtractPropTypes<typeof props>
type triggerMethodsName =
  | 'actionsChanged'
  | 'getImageData'
  | 'putImageData'
  | 'toTempFilePath'

export default /*#__PURE__*/ defineBuiltInComponent({
  inheritAttrs: false,
  name: 'Canvas',
  compatConfig: {
    MODE: 3,
  },
  props,
  computed: {
    id(): Props['canvasId'] {
      return this.canvasId
    },
  },
  setup(props, { emit, slots }) {
    initHidpiOnce()
    const canvas = ref<HTMLCanvasElement | null>(null)
    const sensor = ref<HTMLElement | null>(null)
    const actionsWaiting = ref(false)
    const trigger = useNativeEvent(emit)
    const { $attrs, $excludeAttrs, $listeners } = useAttrs({
      excludeListeners: true,
    })
    const { _listeners } = useListeners(props, $listeners, trigger)

    const { _handleSubscribe, _resize } = useMethods(
      props,
      canvas,
      actionsWaiting
    )

    useSubscribe(
      _handleSubscribe as (
        type: string,
        data: unknown,
        resolve: (res: any) => void
      ) => void,
      useContextInfo(props.canvasId),
      true
    )

    onMounted(() => {
      _resize()
    })

    return () => {
      const { canvasId, disableScroll } = props
      return (
        <uni-canvas
          canvas-id={canvasId}
          disable-scroll={disableScroll}
          {...$attrs.value}
          {...$excludeAttrs.value}
          {..._listeners.value}
        >
          <canvas
            ref={canvas}
            class="uni-canvas-canvas"
            width="300"
            height="150"
          />
          <div style="position: absolute;top: 0;left: 0;width: 100%;height: 100%;overflow: hidden;">
            {slots.default && slots.default()}
          </div>
          <ResizeSensor ref={sensor} onResize={_resize} />
        </uni-canvas>
      )
    }
  },
})

function useListeners(
  props: Props,
  Listeners: Ref<{}>,
  trigger: NativeEventTrigger
) {
  const _listeners = computed(() => {
    let events = ['onTouchstart', 'onTouchmove', 'onTouchend']
    let _$listeners = Listeners.value
    let $listeners = extend(
      {},
      (() => {
        let obj = {}
        for (const key in _$listeners) {
          if (Object.prototype.hasOwnProperty.call(_$listeners, key)) {
            const event = (_$listeners as any)[key]
            ;(obj as any)[key] = event
          }
        }
        return obj
      })()
    )
    events.forEach((event) => {
      let existing = ($listeners as any)[event]
      let eventHandler = []
      if (existing) {
        eventHandler.push(
          withWebEvent(($event) => {
            trigger(
              event.replace('on', '').toLocaleLowerCase(),
              extend(
                {},
                // $event无法直接assign
                (() => {
                  let obj = {}
                  for (const key in $event) {
                    ;(obj as any)[key] = $event[key]
                  }
                  return obj
                })(),
                {
                  touches: processTouches($event.currentTarget, $event.touches),
                  changedTouches: processTouches(
                    $event.currentTarget,
                    $event.changedTouches
                  ),
                }
              ) as unknown as Event
            )
          })
        )
      }
      if (props.disableScroll && event === 'onTouchmove') {
        eventHandler.push(onEventPrevent)
      }
      ;($listeners as any)[event] = eventHandler
    })
    return $listeners
  })

  return {
    _listeners,
  }
}

function useMethods(
  props: Props,
  canvasRef: Ref<HTMLCanvasElement | null>,
  actionsWaiting: Ref<boolean>
) {
  let _actionsDefer: Array<[Actions, boolean, number?]> = []
  let _images: {
    [key: string]: HTMLImageElement & { ready: boolean }
  } = {}

  const _pixelRatio = computed(() => (props.hidpi ? pixelRatio : 1))

  function _resize(size?: { width: number; height: number }) {
    let canvas = canvasRef.value!
    var hasChanged =
      !size ||
      canvas.width !== Math.floor(size.width * _pixelRatio.value) ||
      canvas.height !== Math.floor(size.height * _pixelRatio.value)
    if (!hasChanged) return
    if (canvas.width > 0 && canvas.height > 0) {
      let context = canvas.getContext('2d')!
      let imageData = context.getImageData(0, 0, canvas.width, canvas.height)
      wrapper(canvas, props.hidpi)
      context.putImageData(imageData, 0, 0)
    } else {
      wrapper(canvas, props.hidpi)
    }
  }
  function actionsChanged(
    {
      actions,
      reserve,
    }: {
      actions: Actions
      reserve: boolean
    },
    resolve: (res: any) => void
  ) {
    if (!actions) {
      return
    }
    if (actionsWaiting.value) {
      _actionsDefer.push([actions, reserve])
      return
    }
    let canvas = canvasRef.value!
    let c2d = canvas.getContext('2d')!
    if (!reserve) {
      c2d.fillStyle = '#000000'
      c2d.strokeStyle = '#000000'
      c2d.shadowColor = '#000000'
      c2d.shadowBlur = 0
      c2d.shadowOffsetX = 0
      c2d.shadowOffsetY = 0
      c2d.setTransform(1, 0, 0, 1, 0, 0)
      c2d.clearRect(0, 0, canvas.width, canvas.height)
    }
    preloadImage(actions)
    for (let index = 0; index < actions.length; index++) {
      type MultipleArray = Array<Array<number | string | number[]>>
      type LinearGradient = Parameters<
        CanvasFillStrokeStyles['createLinearGradient']
      >
      const action = actions[index]
      let method = action.method
      const data = action.data
      const actionType = data[0]
      if (/^set/.test(method) && method !== 'setTransform') {
        const method1 = method[3].toLowerCase() + method.slice(4)
        let color: CanvasGradient | string
        if (method1 === 'fillStyle' || method1 === 'strokeStyle') {
          if (actionType === 'normal') {
            color = resolveColor(data[1] as number[])
          } else if (actionType === 'linear') {
            const LinearGradient = c2d.createLinearGradient(
              ...(data[1] as LinearGradient)
            )
            ;(data[2] as unknown as MultipleArray).forEach(function (data2) {
              const offset = data2[0] as number
              const color = resolveColor(data2[1] as number[])
              LinearGradient.addColorStop(offset, color)
            })
            color = LinearGradient
          } else if (actionType === 'radial') {
            let _data = data[1] as number[]
            const x = _data[0]
            const y = _data[1]
            const r = _data[2]
            const LinearGradient = c2d.createRadialGradient(x, y, 0, x, y, r)
            ;(data[2] as unknown as [number, number[]][]).forEach(function (
              data2
            ) {
              const offset = data2[0]
              const color = resolveColor(data2[1])
              LinearGradient.addColorStop(offset, color)
            })
            color = LinearGradient
          } else if (actionType === 'pattern') {
            const loaded = checkImageLoaded(
              data[1] as string,
              actions.slice(index + 1),
              resolve,
              function (image) {
                if (image) {
                  c2d[method1] = c2d.createPattern(image, data[2] as string)!
                }
              }
            )
            if (!loaded) {
              break
            }
            continue
          }
          c2d[method1] = color!
        } else if (method1 === 'globalAlpha') {
          c2d[method1] = Number(actionType) / 255
        } else if (method1 === 'shadow') {
          let shadowArray = [
            'shadowOffsetX',
            'shadowOffsetY',
            'shadowBlur',
            'shadowColor',
          ]
          data.forEach(function (color_, method_) {
            ;(c2d as any)[shadowArray[method_]] =
              shadowArray[method_] === 'shadowColor'
                ? resolveColor(color_ as number[])
                : color_
          })
        } else if (method1 === 'fontSize') {
          const font = (c2d as any).__font__ || c2d.font
          ;(c2d as any).__font__ = c2d.font = font.replace(
            /\d+\.?\d*px/,
            actionType + 'px'
          )
        } else if (method1 === 'lineDash') {
          c2d.setLineDash(actionType as number[])
          c2d.lineDashOffset = (data[1] as number) || 0
        } else if (method1 === 'textBaseline') {
          if (actionType === 'normal') {
            data[0] = 'alphabetic'
          }
          ;(c2d as any)[method1] = actionType
        } else if (method1 === 'font') {
          ;(c2d as any).__font__ = c2d.font = actionType as string
        } else {
          ;(c2d as any)[method1] = actionType
        }
      } else if (method === 'fillPath' || method === 'strokePath') {
        method = method.replace(/Path/, '')
        c2d.beginPath()
        ;(data as Actions).forEach(function (data_) {
          ;(c2d as any)[data_.method].apply(c2d, data_.data)
        })
        ;(c2d as any)[method]()
      } else if (method === 'fillText') {
        // @ts-ignore
        c2d.fillText.apply(c2d, data)
      } else if (method === 'drawImage') {
        let drawImage = (function () {
          let dataArray = [...data]
          let url = dataArray[0] as string
          let otherData = dataArray.slice(1)
          _images = _images || {}
          if (
            checkImageLoaded(
              url,
              actions.slice(index + 1),
              resolve,
              function (image) {
                if (image) {
                  c2d.drawImage.apply(
                    c2d,
                    // @ts-ignore
                    [image].concat(
                      // @ts-ignore
                      [...otherData.slice(4, 8)],
                      [...otherData.slice(0, 4)]
                    )
                  )
                }
              }
            )
          )
            return 'break'
        })()
        if (drawImage === 'break') {
          break
        }
      } else {
        if (method === 'clip') {
          data.forEach(function (data_) {
            // @ts-ignore
            c2d[data_.method].apply(c2d, data_.data)
          })
          c2d.clip()
        } else {
          // @ts-ignore
          c2d[method].apply(c2d, data)
        }
      }
    }
    if (!actionsWaiting.value) {
      resolve({
        errMsg: 'drawCanvas:ok',
      })
    }
  }
  function preloadImage(actions: Actions) {
    actions.forEach(function (action) {
      let method = action.method
      let data = action.data
      let src = ''
      if (method === 'drawImage') {
        src = data[0] as string
        src = $getRealPath(src)
        data[0] = src
      } else if (method === 'setFillStyle' && data[0] === 'pattern') {
        src = data[1] as string
        src = $getRealPath(src)
        data[1] = src
      }
      if (src && !_images[src]) {
        loadImage()
      }
      /**
       * 加载图像
       */
      function loadImage() {
        // @ts-ignore
        const image = (_images[src] = new Image())
        image.onload = function () {
          // @ts-ignore
          image.ready = true
        }

        // 安卓 WebView 除本地路径无跨域问题
        if (__PLATFORM__ === 'app' && navigator.vendor === 'Google Inc.') {
          if (src.indexOf('file://') === 0) {
            image.crossOrigin = 'anonymous'
          }
          image.src = src
          return
        }
        getSameOriginUrl(src)
          .then((src) => {
            image.src = src
          })
          .catch(() => {
            image.src = src
          })
      }
    })
  }
  function checkImageLoaded(
    src: string,
    actions: Actions,
    resolve: (res: any) => void,
    fn: (a: CanvasImageSource) => void
  ) {
    let image = _images[src]
    if (image.ready) {
      fn(image)
      return true
    } else {
      _actionsDefer.unshift([actions, true])
      actionsWaiting.value = true
      image.onload = function () {
        image.ready = true
        fn(image)
        actionsWaiting.value = false
        let actions = _actionsDefer.slice(0)
        _actionsDefer = []
        for (let action = actions.shift(); action; ) {
          actionsChanged(
            {
              actions: action[0],
              reserve: action[1],
            },
            resolve
          )
          action = actions.shift()
        }
      }
      return false
    }
  }
  function getImageData(
    {
      x = 0,
      y = 0,
      width,
      height,
      destWidth,
      destHeight,
      hidpi = true,
      dataType,
      quality = 1,
      type = 'png',
    }: {
      x: number
      y: number
      width: number
      height: number
      destWidth: number
      destHeight: number
      hidpi: boolean
      dataType: string
      quality: number
      type: string
    },
    resolve?: (res: any) => void
  ) {
    const canvas = canvasRef.value!
    let data: string | number[]
    const maxWidth = canvas.offsetWidth - x
    width = width ? Math.min(width, maxWidth) : maxWidth
    const maxHeight = canvas.offsetHeight - y
    height = height ? Math.min(height, maxHeight) : maxHeight
    if (!hidpi) {
      if (!destWidth && !destHeight) {
        destWidth = Math.round(width * _pixelRatio.value)
        destHeight = Math.round(height * _pixelRatio.value)
      } else if (!destWidth) {
        destWidth = Math.round((width / height) * destHeight)
      } else if (!destHeight) {
        destHeight = Math.round((height / width) * destWidth)
      }
    } else {
      destWidth = width
      destHeight = height
    }
    const newCanvas = getTempCanvas(destWidth, destHeight)
    const context = newCanvas.getContext('2d')!
    if (type === 'jpeg' || type === 'jpg') {
      type = 'jpeg'
      context.fillStyle = '#fff'
      context.fillRect(0, 0, destWidth, destHeight)
    }
    // @ts-expect-error
    context.__hidpi__ = true
    // @ts-expect-error
    context.drawImageByCanvas(
      canvas,
      x,
      y,
      width,
      height,
      0,
      0,
      destWidth,
      destHeight,
      false
    )
    let result
    try {
      let compressed
      if (dataType === 'base64') {
        data = newCanvas.toDataURL(`image/${type}`, quality)
      } else {
        const imgData = context.getImageData(0, 0, destWidth, destHeight)
        if (__PLATFORM__ === 'app') {
          data = deflateRaw(imgData.data as any, { to: 'string' })
          compressed = true
        } else {
          // fix [...]展开TypedArray在低版本手机报错的问题，使用Array.prototype.slice
          data = Array.prototype.slice.call(imgData.data)
        }
      }
      result = {
        data,
        compressed,
        width: destWidth,
        height: destHeight,
      }
    } catch (error) {
      result = {
        errMsg: `canvasGetImageData:fail ${error}`,
      }
    }
    newCanvas.height = newCanvas.width = 0
    // @ts-expect-error
    context.__hidpi__ = false
    if (!resolve) {
      return result
    } else {
      resolve(result)
    }
  }
  function putImageData(
    {
      data,
      x,
      y,
      width,
      height,
      compressed,
    }: {
      data: Array<number>
      x: number
      y: number
      width: number
      height: number
      compressed: boolean
    },
    resolve: (res: any) => void
  ) {
    try {
      if (__PLATFORM__ === 'app' && compressed) {
        data = inflateRaw(data) as any
      }
      if (!height) {
        height = Math.round(data.length / 4 / width)
      }
      const canvas = getTempCanvas(width, height)
      const context = canvas.getContext('2d')!
      context.putImageData(
        new ImageData(new Uint8ClampedArray(data), width, height),
        0,
        0
      )
      canvasRef.value!.getContext('2d')!.drawImage(canvas, x, y, width, height)
      canvas.height = canvas.width = 0
    } catch (error) {
      resolve({ errMsg: 'canvasPutImageData:fail' })
      return
    }
    resolve({ errMsg: 'canvasPutImageData:ok' })
  }
  function toTempFilePath(
    {
      x = 0,
      y = 0,
      width,
      height,
      destWidth,
      destHeight,
      fileType,
      quality,
      dirname,
    }: {
      x: number
      y: number
      width: number
      height: number
      destWidth: number
      destHeight: number
      fileType: string
      quality: number
      dirname: string
    },
    resolve: (res: any) => void
  ) {
    const res = getImageData({
      x,
      y,
      width,
      height,
      destWidth,
      destHeight,
      hidpi: false,
      dataType: 'base64',
      type: fileType,
      quality,
    })!
    if (!res.data || !res.data.length) {
      resolve({
        errMsg: res!.errMsg!.replace('canvasPutImageData', 'toTempFilePath'),
      })
      return
    }
    saveImage(res.data as string, dirname, (error, tempFilePath) => {
      let errMsg = `toTempFilePath:${error ? 'fail' : 'ok'}`
      if (error) {
        errMsg += ` ${error.message}`
      }
      resolve({ errMsg, tempFilePath })
    })
  }

  const methods = {
    actionsChanged,
    getImageData,
    putImageData,
    toTempFilePath,
  }

  function _handleSubscribe(
    type: triggerMethodsName,
    data: any,
    resolve: (res: { callbackId: number; data: any }) => void
  ) {
    let method = methods[type]
    if (type.indexOf('_') !== 0 && typeof method === 'function') {
      method(data as any, resolve)
    }
  }

  return extend(methods, {
    _resize,
    _handleSubscribe,
  })
}
