export interface CompassHeadingControllerCallbacks {
  onHeading: (headingDeg: number) => void
  onError: (message: string) => void
}

type SensorSource = 'compass' | 'motion' | null

export type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'

const HEADING_CORRECTION_BY_PROFILE: Record<CompassTuningProfile, number> = {
  smooth: 0.3,
  balanced: 0.4,
  responsive: 0.54,
}

function normalizeHeadingDeg(headingDeg: number): number {
  const normalized = headingDeg % 360
  return normalized < 0 ? normalized + 360 : normalized
}

function normalizeHeadingDeltaDeg(deltaDeg: number): number {
  let normalized = deltaDeg

  while (normalized > 180) {
    normalized -= 360
  }

  while (normalized < -180) {
    normalized += 360
  }

  return normalized
}

function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: number): number {
  return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
}

export class CompassHeadingController {
  callbacks: CompassHeadingControllerCallbacks
  listening: boolean
  source: SensorSource
  compassCallback: ((result: WechatMiniprogram.OnCompassChangeCallbackResult) => void) | null
  motionCallback: ((result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => void) | null
  absoluteHeadingDeg: number | null
  pitchDeg: number | null
  rollDeg: number | null
  motionReady: boolean
  compassReady: boolean
  tuningProfile: CompassTuningProfile

  constructor(callbacks: CompassHeadingControllerCallbacks) {
    this.callbacks = callbacks
    this.listening = false
    this.source = null
    this.compassCallback = null
    this.motionCallback = null
    this.absoluteHeadingDeg = null
    this.pitchDeg = null
    this.rollDeg = null
    this.motionReady = false
    this.compassReady = false
    this.tuningProfile = 'balanced'
  }

  start(): void {
    if (this.listening) {
      return
    }

    this.absoluteHeadingDeg = null
    this.pitchDeg = null
    this.rollDeg = null
    this.motionReady = false
    this.compassReady = false
    this.source = null

    if (typeof wx.startCompass === 'function' && typeof wx.onCompassChange === 'function') {
      this.startCompassSource()
      return
    }

    this.callbacks.onError('当前环境不支持罗盘方向监听')
  }

  stop(): void {
    this.detachCallbacks()

    if (this.motionReady) {
      wx.stopDeviceMotionListening({ complete: () => {} })
    }

    if (this.compassReady) {
      wx.stopCompass({ complete: () => {} })
    }

    this.listening = false
    this.source = null
    this.absoluteHeadingDeg = null
    this.pitchDeg = null
    this.rollDeg = null
    this.motionReady = false
    this.compassReady = false
  }

  destroy(): void {
    this.stop()
  }

  setTuningProfile(profile: CompassTuningProfile): void {
    this.tuningProfile = profile
  }

  startMotionSource(previousMessage: string): void {
    if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
      this.callbacks.onError(previousMessage)
      return
    }

    const callback = (result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => {
      if (typeof result.alpha !== 'number' || Number.isNaN(result.alpha)) {
        return
      }

      this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
        ? result.beta
        : null
      this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
        ? result.gamma
        : null

      this.applyAbsoluteHeading(normalizeHeadingDeg(360 - result.alpha), 'motion')
    }

    this.motionCallback = callback
    wx.onDeviceMotionChange(callback)
    wx.startDeviceMotionListening({
      interval: 'ui',
      success: () => {
        this.motionReady = true
        this.listening = true
        this.source = 'motion'
      },
      fail: (res) => {
        this.detachMotionCallback()
        const motionMessage = res && res.errMsg ? res.errMsg : 'startDeviceMotionListening failed'
        this.callbacks.onError(`${previousMessage}；${motionMessage}`)
      },
    })
  }

  startCompassSource(): void {
    const callback = (result: WechatMiniprogram.OnCompassChangeCallbackResult) => {
      if (typeof result.direction !== 'number' || Number.isNaN(result.direction)) {
        return
      }

      this.applyAbsoluteHeading(normalizeHeadingDeg(result.direction), 'compass')
    }

    this.compassCallback = callback
    wx.onCompassChange(callback)
    wx.startCompass({
      success: () => {
        this.compassReady = true
        this.listening = true
        this.source = 'compass'
      },
      fail: (res) => {
        this.detachCompassCallback()
        this.callbacks.onError(res && res.errMsg ? res.errMsg : 'startCompass failed')
      },
    })
  }

  applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
    const headingCorrection = HEADING_CORRECTION_BY_PROFILE[this.tuningProfile]
    if (this.absoluteHeadingDeg === null) {
      this.absoluteHeadingDeg = headingDeg
    } else {
      this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, headingCorrection)
    }

    this.source = source
    this.callbacks.onHeading(this.absoluteHeadingDeg)
  }

  detachCallbacks(): void {
    this.detachMotionCallback()
    this.detachCompassCallback()
  }

  detachMotionCallback(): void {
    if (!this.motionCallback) {
      return
    }

    if (typeof wx.offDeviceMotionChange === 'function') {
      wx.offDeviceMotionChange(this.motionCallback)
    }
    this.motionCallback = null
  }

  detachCompassCallback(): void {
    if (!this.compassCallback) {
      return
    }

    if (typeof wx.offCompassChange === 'function') {
      wx.offCompassChange(this.compassCallback)
    }
    this.compassCallback = null
  }
}
