export type RGBA = number[]

export type Point = [number, number]

export interface GetPixPointParams {
  width: number
  height: number
}

export interface GetMainColorOption {
  width: number
  height: number
}

export interface InitTypeOption {
  type: 'init'
  imageBitMap: ImageBitmap
  scale?: number
  rotate?: number
  distance?: number
  enableSharpen?: boolean
  basePoint?: Point[]
}

export interface scaleTypeOption {
  type: 'scale'
  scale: number
}

export interface rotateTypeOption {
  type: 'rotate'
  rotate: number
}

export type MessageData = InitTypeOption | scaleTypeOption | rotateTypeOption

/**
 * @description 相似度阀值
 */
const DISTANCE = 45

/**
 * @description 黑色RGBA
 */
const BLACK = [0, 0, 0, 255]

/**
 * @description 白色RGBA
 */
const WHITE = [255, 255, 255, 255]

/**
 * @description 透明色RGBA
 */
const TRANSPARENT = [255, 255, 255, 0]

/**
 * @description 背景色取色点
 */
const COLOR_POINT: Point[] = [
  [0, 0],
  [0.25, 0],
  [0.5, 0],
  [0.75, 0],
  [1, 0],
  [0, 0.25],
  [1, 0.25],
  [0, 0.5],
  [1, 0.5],
  [0, 0.75],
  [1, 0.75],
  [0, 1],
  [0.25, 1],
  [0.5, 1],
  [0.75, 1],
]

const pixelPointsHelper = [0, 1, 2, 3]

/**
 * @description 根据取色点获取图片像素点
 * @params { Object } width canvas宽度
 * @params { Object } height canvas高度
 */

const getPixelPoint = ({ width, height }: GetPixPointParams, colorPoint: Point[] = COLOR_POINT) => {
  return colorPoint.map(([x, y]) => {
    if (x <= 1 && y <= 1) {
      const index = Math.max(Math.ceil(width * x), 0) + Math.max(Math.ceil(height * y) - 1, 0) * width
      return Math.max(index + Math.ceil(x) * -1, 0) * 4
    } else {
      const index = Math.max(x, 0) + Math.max(y - 1, 0) * width - 1
      return Math.max(index, 0) * 4
    }
  })
}

/**
 * @description 计算像素点相似度
 */
const calcPixSim = (rgba: RGBA, base: RGBA = WHITE) => {
  const [r, g, b, a] = rgba
  const [r1, g1, b1, a1] = base
  return Math.sqrt((r - r1) ** 2 + (g - g1) ** 2 + (b - b1) ** 2 + (a - a1) ** 2)
}

/**
 * @description 获取各取色点中出现次数最多的颜色
 */
const getMainColor = (colors: RGBA[]) => {
  let max = 1
  let index = 0
  let num = 0
  const total = colors
    .map((_) => calcPixSim(_))
    .reduce((r, n, i) => {
      r[n] ??= 0
      r[n]++
      if (r[n] > max) {
        max = r[n]
        num = n
        index = i
      }
      return r
    }, {} as Record<number, number>)
  return {
    max,
    index,
    num,
    total,
    mainColor: colors[index],
  }
}

/**
 * @description 遍历imageData
 */
const eachImageData = (data: ImageData, cb: (i: number) => void) => {
  if (!data.data) {
    return
  }
  for (let i = 0; i < data.data.length; i += 4) {
    cb(i)
  }
}

class ImageWorkerController {
  canvas: OffscreenCanvas | null = null
  #imageData: ImageData | null = null
  #imageBitMap: ImageBitmap | null = null
  #context: OffscreenCanvasRenderingContext2D | null = null
  #distance: number = DISTANCE
  #basePoint: Point[] = COLOR_POINT
  #mainColor: RGBA | null = null
  #scale = 1
  #rotate = 0
  #enableSharpen = false
  #blob: Blob | null = null
  #drawing = false
  #initDone = false
  #postTimer: number | null = null

  get drawing() {
    return this.#drawing
  }

  set drawing(v: boolean) {
    this.#drawing = v
    this.post()
  }

  /**
   * @description 判断两个颜色是否相似
   */
  isSimilar(color1: RGBA, color2: RGBA, distance: number = this.#distance) {
    return calcPixSim(color1, color2) <= distance
  }

  /**
   * @description 根据imageData起始下标获取rgba
   */
  getColorFromIndex(index: number) {
    if (!this.#imageData) return []
    return pixelPointsHelper.map((_) => {
      return this.#imageData?.data[index + _]
    }) as unknown as RGBA
  }

  /**
   * @description 根据imageData起始下标设置rgba
   */
  setColorFromIndex = (index: number, color: RGBA) => {
    if (!this.#imageData) return
    for (const _ of pixelPointsHelper) {
      this.#imageData.data[index + _] = color[_]
    }
  }

  /** 获取主色 */
  getMainColorWithPointes({ width, height }: GetMainColorOption) {
    const pixelPoints = getPixelPoint({ width, height }, this.#basePoint)
    const pixelsInfo = pixelPoints.map((_) => this.getColorFromIndex(_))
    const { mainColor } = getMainColor(pixelsInfo)
    return mainColor
  }

  /** 是否透明 */
  isAlphaZero(color: RGBA) {
    return color[-1] === 0
  }

  /** 锐化/移除背景色 */
  async tintImage({ sharpen, tint }: { sharpen?: boolean; tint?: boolean }, updateView: boolean) {
    if (!sharpen && !tint) {
      return
    }
    if (!this.canvas) {
      return
    }
    if (!this.#imageData) {
      return
    }
    const shouldTint = !!tint && !!this.#mainColor && !this.isAlphaZero(this.#mainColor)
    eachImageData(this.#imageData, (index) => {
      if (sharpen && this.isSimilar(this.getColorFromIndex(index), BLACK)) {
        this.setColorFromIndex(index, BLACK)
      }
      if (shouldTint && this.isSimilar(this.getColorFromIndex(index), this.#mainColor as RGBA)) {
        this.setColorFromIndex(index, TRANSPARENT)
      }
    })
    this.#context?.putImageData(this.#imageData, 0, 0)
    this.#imageBitMap = this.canvas.transferToImageBitmap()
    if (updateView) {
      this.imageToCanvas(1)
    }
  }

  /** 缩放 */
  async scaleImage(scale: number) {
    if (!this.#initDone) return
    this.#scale = scale ?? this.#scale
    await this.imageToCanvas(scale)
    if (this.#rotate !== 0) {
      this.rotateImage(this.#rotate)
    }
  }

  /** 旋转 */
  async rotateImage(rotate = this.#rotate) {
    this.#rotate = rotate % 360
    if (!this.#initDone) return
    if (!this.canvas) {
      return
    }
    if (this.#rotate === 0) {
      return this.imageToCanvas(this.#scale)
    }
    const { width, height } = this.canvas
    const W = Math.max(width, height)
    const canvas = new OffscreenCanvas(W, W)
    const context = canvas.getContext('2d')

    if (!context) {
      return
    }
    this.drawing = true
    context.translate(W / 2, W / 2)
    context.rotate(this.#rotate * (Math.PI / 180))
    let x = -W / 2
    let y = -W / 2
    let w = width
    let h = height
    switch (this.#rotate) {
      case 90:
        y = W / 2 - height
        w = height
        h = width
        break
      case 180:
        x = W / 2 - width
        y = W / 2 - height
        break
      case 270:
        x = -width + W / 2
        w = height
        h = width
        break
    }
    context.drawImage(this.canvas, x, y)
    const renderCanvas = new OffscreenCanvas(w, h)
    const renderContext = renderCanvas.getContext('2d')
    renderContext?.drawImage(canvas, 0, 0, w, h, 0, 0, w, h)
    this.#blob = await renderCanvas?.convertToBlob()
    this.drawing = false
  }

  async imageToCanvas(scale = 1, updateView = true, imageBitMap: ImageBitmap | null = this.#imageBitMap) {
    if (!imageBitMap) {
      return
    }
    this.drawing = true
    const width = imageBitMap.width * scale
    const height = imageBitMap.height * scale
    this.canvas = new OffscreenCanvas(width, height)
    this.#context = this.canvas.getContext('2d')
    this.#context?.drawImage(imageBitMap, 0, 0, width, height)
    this.#imageData = this.#context?.getImageData(0, 0, width, height) || null
    this.#blob = await this.canvas.convertToBlob()
    if (updateView) {
      this.drawing = false
    }
  }

  async imageDataUpdate(updateView: boolean) {
    if (this.#imageData && this.canvas) {
      this.#mainColor = this.getMainColorWithPointes({ width: this.canvas.width, height: this.canvas.height })
      await this.tintImage({ sharpen: this.#enableSharpen, tint: true }, updateView)
    }
  }

  async init(option: InitTypeOption) {
    if (!option.imageBitMap) return
    this.#initDone = false
    this.#imageBitMap = option.imageBitMap
    this.#basePoint = option.basePoint ?? COLOR_POINT
    this.#enableSharpen = option.enableSharpen ?? false
    this.#distance = option.distance ?? DISTANCE
    this.#scale = option.scale ?? 1
    this.#rotate = option.rotate ?? 0
    await this.imageToCanvas(1, false)
    await this.imageDataUpdate(this.#scale == 1 && this.#rotate == 0)
    this.#initDone = true
    if (this.#scale !== 1) {
      this.scaleImage(this.#scale)
    } else if (this.#rotate !== 0) {
      this.rotateImage(this.#rotate)
    }
  }

  post() {
    if (this.#postTimer) {
      clearTimeout(this.#postTimer)
    }
    this.#postTimer = self.setTimeout(() => {
      postMessage({ drawing: this.drawing, blob: this.#blob })
    }, 20)
  }
}

const imageController = new ImageWorkerController()

addEventListener('message', (e) => {
  const data = e.data as MessageData

  switch (data.type) {
    case 'init':
      imageController.init(data)
      break
    case 'scale':
      imageController.scaleImage(data.scale)
      break
    case 'rotate':
      imageController.rotateImage(data.rotate)
      break
  }
})
