import {
  MapEngine,
  type MapEngineGameInfoRow,
  type MapEngineGameInfoSnapshot,
  type MapEngineResultSnapshot,
  type MapEngineStageRect,
  type MapEngineViewState,
} from '../../engine/map/mapEngine'
import {
  getBackendSessionContextFromLaunchEnvelope,
  getDemoGameLaunchEnvelope,
  resolveGameLaunchEnvelope,
  type GameLaunchEnvelope,
  type MapPageLaunchOptions,
} from '../../utils/gameLaunch'
import { finishSession, startSession, type BackendSessionFinishSummaryPayload } from '../../utils/backendApi'
import { loadBackendBaseUrl } from '../../utils/backendAuth'
import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
import { type TrackColorPreset } from '../../game/presentation/trackStyleConfig'
import { type GpsMarkerColorPreset } from '../../game/presentation/gpsMarkerStyleConfig'
import { type PlayerTelemetryProfile } from '../../game/telemetry/playerTelemetryProfile'
import {
  DEFAULT_SETTING_LOCKS,
  DEFAULT_STORED_USER_SETTINGS,
  loadStoredUserSettings,
  mergeStoredUserSettings,
  persistStoredUserSettings,
  resolveSystemSettingsState,
  type SystemSettingsConfig,
  type CenterScaleRulerAnchorMode,
  type ResolvedSystemSettingsState,
  type SideButtonPlacement,
  type StoredUserSettings,
} from '../../game/core/systemSettingsState'
import {
  compileRuntimeProfile,
} from '../../game/core/runtimeProfileCompiler'
import {
  clearSessionRecoverySnapshot,
  loadSessionRecoverySnapshot,
  saveSessionRecoverySnapshot,
  type SessionRecoverySnapshot,
} from '../../game/core/sessionRecovery'
type CompassTickData = {
  angle: number
  long: boolean
  major: boolean
}
type CompassLabelData = {
  text: string
  angle: number
  rotateBack: number
  radius: number
  className: string
}
type ScaleRulerMinorTickData = {
  key: string
  topPx: number
  long: boolean
}
type ScaleRulerMajorMarkData = {
  key: string
  topPx: number
  label: string
}
type SideButtonMode = 'shown' | 'hidden'
type SideActionButtonState = 'muted' | 'default' | 'active'
type MapPageData = MapEngineViewState & {
  showDebugPanel: boolean
  showGameInfoPanel: boolean
  showResultScene: boolean
  showSystemSettingsPanel: boolean
  showHeartRateDevicePicker: boolean
  showCenterScaleRuler: boolean
  showPunchHintBanner: boolean
  punchHintFxClass: string
  centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  statusBarHeight: number
  topInsetHeight: number
  hudPanelIndex: number
  configSourceText: string
  mockBridgeUrlDraft: string
  mockHeartRateBridgeUrlDraft: string
  mockDebugLogBridgeUrlDraft: string
  mockChannelIdDraft: string
  gameInfoTitle: string
  gameInfoSubtitle: string
  gameInfoLocalRows: MapEngineGameInfoRow[]
  gameInfoGlobalRows: MapEngineGameInfoRow[]
  resultSceneTitle: string
  resultSceneSubtitle: string
  resultSceneHeroLabel: string
  resultSceneHeroValue: string
  resultSceneRows: MapEngineGameInfoRow[]
  resultSceneCountdownText: string
  panelTimerText: string
  panelTimerMode: 'elapsed' | 'countdown'
  panelMileageText: string
  panelTargetSummaryText: string
  panelDistanceValueText: string
  panelProgressText: string
  panelSpeedValueText: string
  panelTimerFxClass: string
  panelMileageFxClass: string
  panelSpeedFxClass: string
  panelHeartRateFxClass: string
  compassTicks: CompassTickData[]
  compassLabels: CompassLabelData[]
  sideButtonMode: SideButtonMode
  sideButtonPlacement: SideButtonPlacement
  autoRotateEnabled: boolean
  lockAnimationLevel: boolean
  lockTrackMode: boolean
  lockTrackTailLength: boolean
  lockTrackColor: boolean
  lockTrackStyle: boolean
  lockGpsMarkerVisible: boolean
  lockGpsMarkerStyle: boolean
  lockGpsMarkerSize: boolean
  lockGpsMarkerColor: boolean
  lockSideButtonPlacement: boolean
  lockAutoRotate: boolean
  lockCompassTuning: boolean
  lockScaleRulerVisible: boolean
  lockScaleRulerAnchor: boolean
  lockNorthReference: boolean
  lockHeartRateDevice: boolean
  sideToggleIconSrc: string
  sideButton2Class: string
  sideButton4Class: string
  sideButton11Class: string
  sideButton12Class: string
  sideButton13Class: string
  sideButton14Class: string
  sideButton16Class: string
  centerScaleRulerVisible: boolean
  centerScaleRulerCenterXPx: number
  centerScaleRulerZeroYPx: number
  centerScaleRulerHeightPx: number
  centerScaleRulerAxisBottomPx: number
  centerScaleRulerZeroVisible: boolean
  centerScaleRulerZeroLabel: string
  centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  showLeftButtonGroup: boolean
  showRightButtonGroups: boolean
  showBottomDebugButton: boolean
}

function getGlobalTelemetryProfile(): PlayerTelemetryProfile | null {
  const app = getApp<IAppOption>()
  const profile = app.globalData && app.globalData.telemetryPlayerProfile
  return profile ? { ...profile } : null
}

const INTERNAL_BUILD_VERSION = 'map-build-293'
const PUNCH_HINT_AUTO_HIDE_MS = 30000
const PUNCH_HINT_FX_DURATION_MS = 420
const PUNCH_HINT_HAPTIC_GAP_MS = 2400
const SESSION_RECOVERY_PERSIST_INTERVAL_MS = 5000
const RESULT_EXIT_REDIRECT_DELAY_MS = 3000
let currentGameLaunchEnvelope: GameLaunchEnvelope = getDemoGameLaunchEnvelope()
let mapEngine: MapEngine | null = null
let stageCanvasAttached = false
let gameInfoPanelSyncTimer = 0
let centerScaleRulerSyncTimer = 0
let contentAudioRecorder: WechatMiniprogram.RecorderManager | null = null
let contentAudioRecording = false
let centerScaleRulerUpdateTimer = 0
let punchHintDismissTimer = 0
let punchHintFxTimer = 0
let panelTimerFxTimer = 0
let panelMileageFxTimer = 0
let panelSpeedFxTimer = 0
let panelHeartRateFxTimer = 0
let sessionRecoveryPersistTimer = 0
let resultExitRedirectTimer = 0
let resultExitCountdownTimer = 0
let lastPunchHintHapticAt = 0
let currentSystemSettingsConfig: SystemSettingsConfig | undefined
let currentRemoteMapConfig: RemoteMapConfig | undefined
let systemSettingsLockLifetimeActive = false
let syncedBackendSessionStartId = ''
let syncedBackendSessionFinishId = ''
let shouldAutoRestoreRecoverySnapshot = false
let redirectedToResultPage = false
let pendingHeartRateSwitchDeviceName: string | null = null
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
let lastCenterScaleRulerStablePatch: Pick<
  MapPageData,
  | 'centerScaleRulerVisible'
  | 'centerScaleRulerCenterXPx'
  | 'centerScaleRulerZeroYPx'
  | 'centerScaleRulerHeightPx'
  | 'centerScaleRulerAxisBottomPx'
  | 'centerScaleRulerZeroVisible'
  | 'centerScaleRulerZeroLabel'
  | 'centerScaleRulerMinorTicks'
  | 'centerScaleRulerMajorMarks'
> = {
  centerScaleRulerVisible: false,
  centerScaleRulerCenterXPx: 0,
  centerScaleRulerZeroYPx: 0,
  centerScaleRulerHeightPx: 0,
  centerScaleRulerAxisBottomPx: 0,
  centerScaleRulerZeroVisible: false,
  centerScaleRulerZeroLabel: '0 m',
  centerScaleRulerMinorTicks: [],
  centerScaleRulerMajorMarks: [],
}
let centerScaleRulerInputCache: Partial<Pick<
  MapPageData,
  'stageWidth'
  | 'stageHeight'
  | 'zoom'
  | 'centerTileY'
  | 'tileSizePx'
  | 'previewScale'
>> = {}

const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
  'buildVersion',
  'renderMode',
  'projectionMode',
  'mapReady',
  'mapReadyText',
  'mapName',
  'configStatusText',
  'deviceHeadingText',
  'devicePoseText',
  'headingConfidenceText',
  'accelerometerText',
  'gyroscopeText',
  'deviceMotionText',
  'compassSourceText',
  'compassTuningProfile',
  'compassTuningProfileText',
  'northReferenceButtonText',
  'autoRotateSourceText',
  'autoRotateCalibrationText',
  'northReferenceText',
  'centerText',
  'tileSource',
  'visibleTileCount',
  'readyTileCount',
  'memoryTileCount',
  'diskTileCount',
  'memoryHitCount',
  'diskHitCount',
  'networkFetchCount',
  'cacheHitRateText',
  'locationSourceMode',
  'locationSourceText',
  'mockBridgeConnected',
  'mockBridgeStatusText',
  'mockBridgeUrlText',
  'mockCoordText',
  'mockSpeedText',
  'gpsCoordText',
  'heartRateSourceMode',
  'heartRateSourceText',
  'heartRateConnected',
  'heartRateStatusText',
  'heartRateDeviceText',
  'heartRateScanText',
  'heartRateDiscoveredDevices',
  'mockHeartRateBridgeConnected',
  'mockHeartRateBridgeStatusText',
  'mockHeartRateBridgeUrlText',
  'mockHeartRateText',
])

const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
  'showCenterScaleRuler',
  'centerScaleRulerAnchorMode',
  'stageWidth',
  'stageHeight',
  'topInsetHeight',
  'zoom',
  'centerTileY',
  'tileSizePx',
  'previewScale',
])

const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
  'stageWidth',
  'stageHeight',
  'zoom',
  'centerTileY',
  'tileSizePx',
  'previewScale',
]

const RULER_ONLY_VIEW_KEYS = new Set<string>([
  'zoom',
  'centerTileX',
  'centerTileY',
  'tileSizePx',
  'previewScale',
  'stageWidth',
  'stageHeight',
  'stageLeft',
  'stageTop',
])

const SIDE_BUTTON_DEP_KEYS = new Set<string>([
  'sideButtonMode',
  'showGameInfoPanel',
  'showCenterScaleRuler',
  'centerScaleRulerAnchorMode',
  'skipButtonEnabled',
  'gameSessionStatus',
  'gpsLockEnabled',
  'gpsLockAvailable',
])

function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
  return Object.keys(patch).some((key) => keys.has(key))
}

function filterDebugOnlyPatch(
  patch: Partial<MapPageData>,
  includeDebugFields: boolean,
  includeRulerFields: boolean,
): Partial<MapPageData> {
  if (includeDebugFields && includeRulerFields) {
    return patch
  }

  const filteredPatch: Partial<MapPageData> = {}
  for (const [key, value] of Object.entries(patch)) {
    if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
      continue
    }
    if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
      continue
    }
    {
      ;(filteredPatch as Record<string, unknown>)[key] = value
    }
  }
  return filteredPatch
}

function clearGameInfoPanelSyncTimer() {
  if (gameInfoPanelSyncTimer) {
    clearTimeout(gameInfoPanelSyncTimer)
    gameInfoPanelSyncTimer = 0
  }
}

function clearCenterScaleRulerSyncTimer() {
  if (centerScaleRulerSyncTimer) {
    clearTimeout(centerScaleRulerSyncTimer)
    centerScaleRulerSyncTimer = 0
  }
}

function clearCenterScaleRulerUpdateTimer() {
  if (centerScaleRulerUpdateTimer) {
    clearTimeout(centerScaleRulerUpdateTimer)
    centerScaleRulerUpdateTimer = 0
  }
}

function clearPunchHintDismissTimer() {
  if (punchHintDismissTimer) {
    clearTimeout(punchHintDismissTimer)
    punchHintDismissTimer = 0
  }
}

function clearPunchHintFxTimer() {
  if (punchHintFxTimer) {
    clearTimeout(punchHintFxTimer)
    punchHintFxTimer = 0
  }
}

function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
  const timerMap = {
    timer: panelTimerFxTimer,
    mileage: panelMileageFxTimer,
    speed: panelSpeedFxTimer,
    heartRate: panelHeartRateFxTimer,
  }
  const timer = timerMap[key]
  if (timer) {
    clearTimeout(timer)
  }
  if (key === 'timer') {
    panelTimerFxTimer = 0
  } else if (key === 'mileage') {
    panelMileageFxTimer = 0
  } else if (key === 'speed') {
    panelSpeedFxTimer = 0
  } else {
    panelHeartRateFxTimer = 0
  }
}

function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
  for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
    if (Object.prototype.hasOwnProperty.call(patch, key)) {
      ;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
        (patch as Record<string, unknown>)[key]
    }
  }
}

function updateStoredUserSettings(patch: Partial<StoredUserSettings>) {
  persistStoredUserSettings(
    mergeStoredUserSettings(loadStoredUserSettings(), patch),
  )
}

function loadStoredMockChannelId(): string {
  try {
    const value = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
    if (typeof value === 'string' && value.trim().length > 0) {
      return value.trim()
    }
  } catch (_error) {
    // Ignore storage read failures and fall back to default.
  }
  return 'default'
}

function persistMockChannelId(channelId: string) {
  try {
    wx.setStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY, channelId)
  } catch (_error) {
    // Ignore storage write failures in debug preference persistence.
  }
}

function loadMockAutoConnectEnabled(): boolean {
  try {
    return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
  } catch (_error) {
    return false
  }
}

function persistMockAutoConnectEnabled(enabled: boolean) {
  try {
    wx.setStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY, enabled)
  } catch (_error) {
    // Ignore storage write failures in debug preference persistence.
  }
}

function buildResolvedSystemSettingsPatch(
  resolvedSettings: ResolvedSystemSettingsState,
): Partial<MapPageData> {
  return {
    ...resolvedSettings.values,
    ...resolvedSettings.locks,
    autoRotateEnabled: resolvedSettings.values.autoRotateEnabled,
    sideButtonPlacement: resolvedSettings.values.sideButtonPlacement,
    showCenterScaleRuler: resolvedSettings.values.showCenterScaleRuler,
    centerScaleRulerAnchorMode: resolvedSettings.values.centerScaleRulerAnchorMode,
  }
}

function isSystemSettingsLockLifetimeActive(): boolean {
  return systemSettingsLockLifetimeActive
}

function clearSessionRecoveryPersistTimer() {
  if (sessionRecoveryPersistTimer) {
    clearInterval(sessionRecoveryPersistTimer)
    sessionRecoveryPersistTimer = 0
  }
}

function clearResultExitRedirectTimer() {
  if (resultExitRedirectTimer) {
    clearTimeout(resultExitRedirectTimer)
    resultExitRedirectTimer = 0
  }
}

function clearResultExitCountdownTimer() {
  if (resultExitCountdownTimer) {
    clearInterval(resultExitCountdownTimer)
    resultExitCountdownTimer = 0
  }
}

function navigateAwayFromMapAfterCancel() {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    wx.navigateBack({
      delta: 1,
    })
    return
  }

  wx.redirectTo({
    url: '/pages/home/home',
  })
}

function hasExplicitLaunchOptions(options?: MapPageLaunchOptions | null): boolean {
  if (!options) {
    return false
  }

  return !!(
    options.launchId
    || options.preset
    || options.configUrl
    || options.competitionId
    || options.eventId
    || options.sessionId
    || options.launchRequestId
  )
}

function getCurrentBackendSessionContext(): { sessionId: string; sessionToken: string } | null {
  return getBackendSessionContextFromLaunchEnvelope(currentGameLaunchEnvelope)
}

function getCurrentBackendBaseUrl(): string {
  const app = getApp<IAppOption>()
  if (app.globalData && app.globalData.backendBaseUrl) {
    return app.globalData.backendBaseUrl
  }
  return loadBackendBaseUrl()
}

function buildSideButtonVisibility(mode: SideButtonMode) {
  return {
    sideButtonMode: mode,
    showLeftButtonGroup: mode === 'shown',
    showRightButtonGroups: false,
    showBottomDebugButton: true,
  }
}

function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  return currentMode === 'shown' ? 'hidden' : 'shown'
}
function buildCompassTicks(): CompassTickData[] {
  const ticks: CompassTickData[] = []
  for (let angle = 0; angle < 360; angle += 5) {
    ticks.push({
      angle,
      long: angle % 15 === 0,
      major: angle % 45 === 0,
    })
  }
  return ticks
}
function buildCompassLabels(): CompassLabelData[] {
  return [
    { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
    { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
    { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
    { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
    { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
    { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
    { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
    { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  ]
}
function getFallbackStageRect(): MapEngineStageRect {
  const systemInfo = wx.getSystemInfoSync()
  const width = Math.max(320, systemInfo.windowWidth)
  const height = Math.max(280, systemInfo.windowHeight)

  return {
    width,
    height,
    left: 0,
    top: 0,
  }
}

function getSideToggleIconSrc(mode: SideButtonMode): string {
  if (mode === 'hidden') {
    return '../../assets/btn_more1.png'
  }
  return '../../assets/btn_more3.png'
}

function getSideActionButtonClass(state: SideActionButtonState): string {
  if (state === 'muted') {
    return 'map-side-button map-side-button--muted'
  }
  if (state === 'active') {
    return 'map-side-button map-side-button--active'
  }
  return 'map-side-button map-side-button--default'
}

function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
    ? 'muted'
    : data.gpsLockEnabled
      ? 'active'
      : 'default'
  const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'running' ? 'active' : 'muted'
  const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
  const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
    ? 'muted'
    : data.centerScaleRulerAnchorMode === 'compass-center'
      ? 'active'
      : 'default'
  const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'

  return {
    sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
    sideButton2Class: getSideActionButtonClass(sideButton2State),
    sideButton4Class: getSideActionButtonClass(sideButton4State),
    sideButton11Class: getSideActionButtonClass(sideButton11State),
    sideButton12Class: getSideActionButtonClass(sideButton12State),
    sideButton13Class: getSideActionButtonClass(sideButton13State),
    sideButton14Class: getSideActionButtonClass(sideButton14State),
    sideButton16Class: getSideActionButtonClass(sideButton16State),
  }
}

function getRpxUnitInPx(): number {
  const systemInfo = wx.getSystemInfoSync()
  return systemInfo.windowWidth / 750
}

function worldTileYToLat(worldTileY: number, zoom: number): number {
  const scale = Math.pow(2, zoom)
  const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  return (180 / Math.PI) * Math.atan(Math.sinh(n))
}

function getNiceDistanceMeters(rawDistanceMeters: number): number {
  if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
    return 50
  }

  const exponent = Math.floor(Math.log10(rawDistanceMeters))
  const base = Math.pow(10, exponent)
  const normalized = rawDistanceMeters / base

  if (normalized <= 1) {
    return base
  }
  if (normalized <= 2) {
    return 2 * base
  }
  if (normalized <= 5) {
    return 5 * base
  }
  return 10 * base
}

function formatScaleDistanceLabel(distanceMeters: number): string {
  if (distanceMeters >= 1000) {
    const distanceKm = distanceMeters / 1000
    const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
    return `${formatted.replace(/\.0$/, '')} km`
  }

  return `${Math.round(distanceMeters)} m`
}

function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  if (!data.showCenterScaleRuler) {
    lastCenterScaleRulerStablePatch = {
      centerScaleRulerVisible: false,
      centerScaleRulerCenterXPx: 0,
      centerScaleRulerZeroYPx: 0,
      centerScaleRulerHeightPx: 0,
      centerScaleRulerAxisBottomPx: 0,
      centerScaleRulerZeroVisible: false,
      centerScaleRulerZeroLabel: '0 m',
      centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
      centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
    }
    return { ...lastCenterScaleRulerStablePatch }
  }

  if (!data.stageWidth || !data.stageHeight) {
    return { ...lastCenterScaleRulerStablePatch }
  }

  const topPadding = 12
  const rpxUnitPx = getRpxUnitInPx()
  const compassBottomPaddingPx = 248 * rpxUnitPx
  const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  const compassOcclusionPaddingPx = 10 * rpxUnitPx
  const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
    ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
    : Math.round(data.stageHeight / 2)
  const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
    ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
    : 0

  if (
    !data.tileSizePx
    || !Number.isFinite(data.zoom)
    || !Number.isFinite(data.centerTileY)
  ) {
    return {
      ...lastCenterScaleRulerStablePatch,
      centerScaleRulerVisible: true,
      centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
      centerScaleRulerZeroYPx: zeroYPx,
      centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
      centerScaleRulerAxisBottomPx: coveredBottomPx,
      centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
    }
  }

  const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  const metersPerPixel = metersPerTile / data.tileSizePx
  const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  const rulerHeight = Math.floor(zeroYPx - topPadding)

  if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
    return {
      ...lastCenterScaleRulerStablePatch,
      centerScaleRulerVisible: true,
      centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
      centerScaleRulerZeroYPx: zeroYPx,
      centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
      centerScaleRulerAxisBottomPx: coveredBottomPx,
      centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
    }
  }

  const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  const minorDistanceMeters = labelDistanceMeters / 8
  const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  const visibleTopLimitPx = rulerHeight - coveredBottomPx
  const minorTicks: ScaleRulerMinorTickData[] = []
  const majorMarks: ScaleRulerMajorMarkData[] = []

  for (let index = 1; index <= 200; index += 1) {
    const topPx = Math.round(rulerHeight - index * minorStepPx)
    if (topPx < 0) {
      break
    }
    if (topPx >= visibleTopLimitPx) {
      continue
    }

    const isHalfMajor = index % 4 === 0
    const isLabelMajor = index % 8 === 0
    minorTicks.push({
      key: `minor-${index}`,
      topPx,
      long: isHalfMajor,
    })

    if (isLabelMajor) {
      majorMarks.push({
        key: `major-${index}`,
        topPx,
        label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
      })
    }
  }

  lastCenterScaleRulerStablePatch = {
    centerScaleRulerVisible: true,
    centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
    centerScaleRulerZeroYPx: zeroYPx,
    centerScaleRulerHeightPx: rulerHeight,
    centerScaleRulerAxisBottomPx: coveredBottomPx,
    centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
    centerScaleRulerZeroLabel: '0 m',
    centerScaleRulerMinorTicks: minorTicks,
    centerScaleRulerMajorMarks: majorMarks,
  }
  return { ...lastCenterScaleRulerStablePatch }
}

function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  return {
    title: '当前游戏',
    subtitle: '未开始',
    localRows: [],
    globalRows: [
      { label: '全球积分', value: '未接入' },
      { label: '全球排名', value: '未接入' },
      { label: '在线人数', value: '未接入' },
      { label: '队伍状态', value: '未接入' },
      { label: '实时广播', value: '未接入' },
    ],
  }
}

function buildEmptyResultSceneSnapshot(): MapEngineResultSnapshot {
  return {
    title: '本局结果',
    subtitle: '未开始',
    heroLabel: '本局用时',
    heroValue: '--',
    rows: [],
  }
}

function buildRuntimeSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInfoRow[] {
  const runtime = envelope.runtime
  const variantName = envelope.variant ? (envelope.variant.variantName || envelope.variant.variantId || null) : null
  const variantRouteCode = envelope.variant ? (envelope.variant.routeCode || null) : null
  if (!runtime) {
    return []
  }

  const rows: MapEngineGameInfoRow[] = []
  rows.push({ label: '运行绑定', value: runtime.runtimeBindingId || '--' })
  rows.push({ label: '地点', value: runtime.placeName || runtime.placeId || '--' })
  rows.push({ label: '地图', value: runtime.mapName || runtime.mapId || '--' })
  rows.push({ label: '赛道集', value: runtime.courseSetId || '--' })
  rows.push({ label: '赛道版本', value: runtime.courseVariantId || variantName || '--' })
  rows.push({ label: 'RouteCode', value: runtime.routeCode || variantRouteCode || '--' })
  rows.push({ label: '瓦片版本', value: runtime.tileReleaseId || '--' })
  return rows
}

Page({
  data: {
      showDebugPanel: false,
      showGameInfoPanel: false,
      showResultScene: false,
      showSystemSettingsPanel: false,
      showHeartRateDevicePicker: false,
      showCenterScaleRuler: false,
    statusBarHeight: 0,
    topInsetHeight: 12,
    hudPanelIndex: 0,
    configSourceText: '顺序赛配置',
    centerScaleRulerAnchorMode: DEFAULT_STORED_USER_SETTINGS.centerScaleRulerAnchorMode,
    punchHintFxClass: '',
    autoRotateEnabled: DEFAULT_STORED_USER_SETTINGS.autoRotateEnabled,
    ...DEFAULT_SETTING_LOCKS,
    gameInfoTitle: '当前游戏',
    gameInfoSubtitle: '未开始',
    gameInfoLocalRows: [],
    gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
    resultSceneTitle: '本局结果',
    resultSceneSubtitle: '未开始',
    resultSceneHeroLabel: '本局用时',
    resultSceneHeroValue: '--',
    resultSceneRows: buildEmptyResultSceneSnapshot().rows,
    resultSceneCountdownText: '',
    panelTimerText: '00:00:00',
    panelTimerMode: 'elapsed',
    panelMileageText: '0m',
    panelActionTagText: '目标',
    panelDistanceTagText: '点距',
    panelTargetSummaryText: '等待选择目标',
    panelDistanceValueText: '--',
    panelDistanceUnitText: '',
    panelProgressText: '0/0',
    showPunchHintBanner: true,
    sideButtonPlacement: 'left',
    gameSessionStatus: 'idle',
    gameModeText: '顺序赛',
    gpsLockEnabled: false,
    gpsLockAvailable: false,
    locationSourceMode: 'real',
    locationSourceText: '真实定位',
    mockBridgeConnected: false,
    mockBridgeStatusText: '未连接',
    mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
    mockChannelIdText: 'default',
    mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
    mockChannelIdDraft: 'default',
    mockCoordText: '--',
    mockSpeedText: '--',
    heartRateSourceMode: 'real',
    heartRateSourceText: '真实心率',
    mockHeartRateBridgeConnected: false,
    mockHeartRateBridgeStatusText: '未连接',
    mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
    mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
    mockHeartRateText: '--',
    mockDebugLogBridgeConnected: false,
    mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
    mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
    mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
    heartRateScanText: '未扫描',
    heartRateDiscoveredDevices: [],
    panelSpeedValueText: '0',
    panelTelemetryTone: 'blue',
    trackDisplayMode: DEFAULT_STORED_USER_SETTINGS.trackDisplayMode,
    trackTailLength: DEFAULT_STORED_USER_SETTINGS.trackTailLength,
    trackColorPreset: DEFAULT_STORED_USER_SETTINGS.trackColorPreset,
    trackStyleProfile: DEFAULT_STORED_USER_SETTINGS.trackStyleProfile,
    gpsMarkerVisible: DEFAULT_STORED_USER_SETTINGS.gpsMarkerVisible,
    gpsMarkerStyle: DEFAULT_STORED_USER_SETTINGS.gpsMarkerStyle,
    gpsMarkerSize: DEFAULT_STORED_USER_SETTINGS.gpsMarkerSize,
    gpsMarkerColorPreset: DEFAULT_STORED_USER_SETTINGS.gpsMarkerColorPreset,
    gpsLogoStatusText: '未配置',
    gpsLogoSourceText: '--',
    panelHeartRateZoneNameText: '--',
    panelHeartRateZoneRangeText: '',
    heartRateConnected: false,
    heartRateStatusText: '心率带未连接',
    heartRateDeviceText: '--',
    panelHeartRateValueText: '--',
    panelHeartRateUnitText: '',
    panelCaloriesValueText: '0',
    panelCaloriesUnitText: 'kcal',
    panelAverageSpeedValueText: '0',
    panelAverageSpeedUnitText: 'km/h',
    panelAccuracyValueText: '--',
    panelAccuracyUnitText: '',
    deviceHeadingText: '--',
    devicePoseText: '竖持',
    headingConfidenceText: '低',
    accelerometerText: '--',
    gyroscopeText: '--',
    deviceMotionText: '--',
    compassSourceText: '无数据',
    compassTuningProfile: DEFAULT_STORED_USER_SETTINGS.compassTuningProfile,
    compassTuningProfileText: '平衡',
    punchButtonText: '打点',
    punchButtonEnabled: false,
    skipButtonEnabled: false,
    punchHintText: '等待进入检查点范围',
    punchFeedbackVisible: false,
    punchFeedbackText: '',
    punchFeedbackTone: 'neutral',
    contentCardVisible: false,
    contentCardTemplate: 'story',
    contentCardTitle: '',
    contentCardBody: '',
    contentCardActions: [],
    contentQuizVisible: false,
    contentQuizQuestionText: '',
    contentQuizCountdownText: '',
    contentQuizOptions: [],
    contentQuizFeedbackVisible: false,
    contentQuizFeedbackText: '',
    contentQuizFeedbackTone: 'neutral',
    punchButtonFxClass: '',
    panelProgressFxClass: '',
    panelDistanceFxClass: '',
    punchFeedbackFxClass: '',
    contentCardFxClass: '',
    mapPulseVisible: false,
    mapPulseLeftPx: 0,
    mapPulseTopPx: 0,
    mapPulseFxClass: '',
    stageFxVisible: false,
    stageFxClass: '',
    centerScaleRulerVisible: false,
    centerScaleRulerCenterXPx: 0,
    centerScaleRulerZeroYPx: 0,
    centerScaleRulerHeightPx: 0,
    centerScaleRulerAxisBottomPx: 0,
    centerScaleRulerZeroVisible: false,
    centerScaleRulerZeroLabel: '0 m',
    centerScaleRulerMinorTicks: [],
    centerScaleRulerMajorMarks: [],
    compassTicks: buildCompassTicks(),
    compassLabels: buildCompassLabels(),
    ...buildSideButtonVisibility('shown'),
    ...buildSideButtonState({
      sideButtonMode: 'shown',
      showGameInfoPanel: false,
      showSystemSettingsPanel: false,
      showCenterScaleRuler: false,
      centerScaleRulerAnchorMode: 'screen-center',
      skipButtonEnabled: false,
      gameSessionStatus: 'idle',
      gpsLockEnabled: false,
      gpsLockAvailable: false,
    }),
  } as unknown as MapPageData,

  onLoad(options: MapPageLaunchOptions) {
    clearSessionRecoveryPersistTimer()
    clearResultExitRedirectTimer()
    clearResultExitCountdownTimer()
    syncedBackendSessionStartId = ''
    syncedBackendSessionFinishId = ''
    redirectedToResultPage = false
    shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
    currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
    if (!hasExplicitLaunchOptions(options)) {
      const recoverySnapshot = loadSessionRecoverySnapshot()
      if (recoverySnapshot) {
        currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
      }
    }
    currentSystemSettingsConfig = undefined
    currentRemoteMapConfig = undefined
    systemSettingsLockLifetimeActive = false
    const storedMockChannelId = loadStoredMockChannelId()
    const shouldAutoConnectMockSources = loadMockAutoConnectEnabled()
    const systemInfo = wx.getSystemInfoSync()
    const statusBarHeight = systemInfo.statusBarHeight || 0
    const menuButtonRect = wx.getMenuButtonBoundingClientRect()
    const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight

    if (mapEngine) {
      mapEngine.destroy()
      mapEngine = null
    }

      mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
        onData: (patch) => {
        const nextPatch = patch as Partial<MapPageData>
        const includeDebugFields = this.data.showDebugPanel
        const includeRulerFields = this.data.showCenterScaleRuler
        let shouldSyncRuntimeSystemSettings = false
        let nextLockLifetimeActive = isSystemSettingsLockLifetimeActive()
        let heartRateSwitchToastText = ''
        const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
          ...nextPatch,
        }, includeDebugFields, includeRulerFields)

        if (
          typeof nextPatch.mockBridgeUrlText === 'string'
          && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
        ) {
          nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
        }

        if (
          typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
          && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
        ) {
          nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
        }

        if (
          typeof nextPatch.mockDebugLogBridgeUrlText === 'string'
          && this.data.mockDebugLogBridgeUrlDraft === this.data.mockDebugLogBridgeUrlText
        ) {
          nextData.mockDebugLogBridgeUrlDraft = nextPatch.mockDebugLogBridgeUrlText
        }

        if (
          typeof nextPatch.mockChannelIdText === 'string'
          && this.data.mockChannelIdDraft === this.data.mockChannelIdText
        ) {
          nextData.mockChannelIdDraft = nextPatch.mockChannelIdText
        }

        updateCenterScaleRulerInputCache(nextPatch)

        const mergedData = {
          ...centerScaleRulerInputCache,
          ...this.data,
          ...nextData,
        } as MapPageData

        const derivedPatch: Partial<MapPageData> = {}
        if (typeof nextPatch.orientationMode === 'string') {
          nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
        }
        if (
          this.data.showCenterScaleRuler
          && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
        ) {
          clearCenterScaleRulerUpdateTimer()
          Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
        }

        if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
          Object.assign(derivedPatch, buildSideButtonState(mergedData))
        }

        if (typeof nextPatch.punchHintText === 'string') {
          const nextHintText = nextPatch.punchHintText.trim()
          if (nextHintText !== this.data.punchHintText) {
            clearPunchHintDismissTimer()
            clearPunchHintFxTimer()
            nextData.showPunchHintBanner = nextHintText.length > 0
            if (nextHintText.length > 0) {
              nextData.punchHintFxClass = 'game-punch-hint--fx-enter'
              punchHintFxTimer = setTimeout(() => {
                punchHintFxTimer = 0
                this.setData({
                  punchHintFxClass: '',
                })
              }, PUNCH_HINT_FX_DURATION_MS) as unknown as number
              const now = Date.now()
            if (mapEngine && now - lastPunchHintHapticAt >= PUNCH_HINT_HAPTIC_GAP_MS) {
              mapEngine.playPunchHintHaptic()
              lastPunchHintHapticAt = now
            }
              punchHintDismissTimer = setTimeout(() => {
                punchHintDismissTimer = 0
                this.setData({
                  showPunchHintBanner: false,
                })
              }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
            }
          } else if (!nextHintText) {
            clearPunchHintDismissTimer()
            clearPunchHintFxTimer()
            nextData.showPunchHintBanner = false
            nextData.punchHintFxClass = ''
          }
        }

        const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
          ? nextPatch.animationLevel
          : this.data.animationLevel
        let shouldSyncBackendSessionStart = false
        let backendSessionFinishStatus: 'finished' | 'failed' | null = null
        let shouldOpenResultExitPrompt = false
        let resultPageSnapshot: MapEngineResultSnapshot | null = null

        if (nextAnimationLevel === 'lite') {
          clearHudFxTimer('timer')
          clearHudFxTimer('mileage')
          clearHudFxTimer('speed')
          clearHudFxTimer('heartRate')
          nextData.panelTimerFxClass = ''
          nextData.panelMileageFxClass = ''
          nextData.panelSpeedFxClass = ''
          nextData.panelHeartRateFxClass = ''
        } else {
          if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
            clearHudFxTimer('timer')
            nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
            panelTimerFxTimer = setTimeout(() => {
              panelTimerFxTimer = 0
              this.setData({ panelTimerFxClass: '' })
            }, 320) as unknown as number
          }

          if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
            clearHudFxTimer('mileage')
            nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
            panelMileageFxTimer = setTimeout(() => {
              panelMileageFxTimer = 0
              this.setData({ panelMileageFxClass: '' })
            }, 360) as unknown as number
          }

          if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
            clearHudFxTimer('speed')
            nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
            panelSpeedFxTimer = setTimeout(() => {
              panelSpeedFxTimer = 0
              this.setData({ panelSpeedFxClass: '' })
            }, 360) as unknown as number
          }

          if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
            clearHudFxTimer('heartRate')
            nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
            panelHeartRateFxTimer = setTimeout(() => {
              panelHeartRateFxTimer = 0
              this.setData({ panelHeartRateFxClass: '' })
            }, 400) as unknown as number
          }
        }

        if (typeof nextPatch.gameSessionStatus === 'string') {
          if (
            nextPatch.gameSessionStatus !== this.data.gameSessionStatus
            && (nextPatch.gameSessionStatus === 'finished' || nextPatch.gameSessionStatus === 'failed')
          ) {
            systemSettingsLockLifetimeActive = false
            nextLockLifetimeActive = false
            shouldSyncRuntimeSystemSettings = true
            clearSessionRecoverySnapshot()
            clearSessionRecoveryPersistTimer()
            clearResultExitRedirectTimer()
            clearResultExitCountdownTimer()
            resultPageSnapshot = mapEngine ? mapEngine.getResultSceneSnapshot() : null
            nextData.showResultScene = true
            nextData.showDebugPanel = false
            nextData.showGameInfoPanel = false
            nextData.showSystemSettingsPanel = false
            clearGameInfoPanelSyncTimer()
            backendSessionFinishStatus = nextPatch.gameSessionStatus === 'finished' ? 'finished' : 'failed'
            shouldOpenResultExitPrompt = true
            if (resultPageSnapshot) {
              nextData.resultSceneTitle = resultPageSnapshot.title
              nextData.resultSceneSubtitle = resultPageSnapshot.subtitle
              nextData.resultSceneHeroLabel = resultPageSnapshot.heroLabel
              nextData.resultSceneHeroValue = resultPageSnapshot.heroValue
              nextData.resultSceneRows = resultPageSnapshot.rows
            }
            nextData.resultSceneCountdownText = '3 秒后自动进入成绩页'
          } else if (
            nextPatch.gameSessionStatus !== this.data.gameSessionStatus
            && nextPatch.gameSessionStatus === 'idle'
            && !isSystemSettingsLockLifetimeActive()
          ) {
            nextLockLifetimeActive = false
            shouldSyncRuntimeSystemSettings = true
            clearSessionRecoverySnapshot()
            clearSessionRecoveryPersistTimer()
            clearResultExitRedirectTimer()
            clearResultExitCountdownTimer()
          } else if (
            nextPatch.gameSessionStatus !== this.data.gameSessionStatus
            && nextPatch.gameSessionStatus === 'running'
          ) {
            shouldSyncBackendSessionStart = true
          } else if (nextPatch.gameSessionStatus === 'running' || nextPatch.gameSessionStatus === 'idle') {
            nextData.showResultScene = false
          }
        }

        if (
          pendingHeartRateSwitchDeviceName
          && nextPatch.heartRateConnected === true
          && typeof nextPatch.heartRateDeviceText === 'string'
        ) {
          const connectedDeviceName = nextPatch.heartRateDeviceText.trim()
          if (connectedDeviceName && connectedDeviceName === pendingHeartRateSwitchDeviceName) {
            heartRateSwitchToastText = `已切换到 ${connectedDeviceName}`
            nextData.statusText = `已切换心率带：${connectedDeviceName}`
            pendingHeartRateSwitchDeviceName = null
          }
        }

        if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
          this.setData({
            ...nextData,
            ...derivedPatch,
          }, () => {
            if (typeof nextPatch.gameSessionStatus === 'string') {
              this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
            }
            if (shouldSyncBackendSessionStart) {
              this.syncBackendSessionStart()
            }
            if (backendSessionFinishStatus) {
              this.syncBackendSessionFinish(backendSessionFinishStatus)
            }
              if (shouldOpenResultExitPrompt && resultPageSnapshot) {
                this.stashPendingResultSnapshot(resultPageSnapshot)
                this.presentResultExitPrompt()
              }
              if (heartRateSwitchToastText) {
                wx.showToast({
                  title: `${heartRateSwitchToastText}，并设为首选设备`,
                  icon: 'none',
                  duration: 1800,
                })
              }
              if (shouldSyncRuntimeSystemSettings) {
                this.applyRuntimeSystemSettings(nextLockLifetimeActive)
              }
            if (this.data.showGameInfoPanel) {
              this.scheduleGameInfoPanelSnapshotSync()
            }
          })
        } else {
          if (typeof nextPatch.gameSessionStatus === 'string') {
            this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
          }
          if (shouldSyncBackendSessionStart) {
            this.syncBackendSessionStart()
          }
          if (backendSessionFinishStatus) {
            this.syncBackendSessionFinish(backendSessionFinishStatus)
          }
          if (shouldOpenResultExitPrompt && resultPageSnapshot) {
            this.stashPendingResultSnapshot(resultPageSnapshot)
            this.presentResultExitPrompt()
          }
          if (shouldSyncRuntimeSystemSettings) {
            this.applyRuntimeSystemSettings(nextLockLifetimeActive)
          }
          if (this.data.showGameInfoPanel) {
            this.scheduleGameInfoPanelSnapshotSync()
          }
        }
        },
        onOpenH5Experience: (request) => {
          this.openH5Experience(request)
        },
      })

    mapEngine.applyTelemetryPlayerProfile(getGlobalTelemetryProfile())

    const systemSettingsState = resolveSystemSettingsState(undefined, undefined, false)
    const initialSystemSettings = systemSettingsState.values
    mapEngine.applyCompiledSettingsProfile({
      values: initialSystemSettings,
      locks: systemSettingsState.locks,
      lockLifetimeActive: false,
    })

    mapEngine.setDiagnosticUiEnabled(false)
    centerScaleRulerInputCache = {
      stageWidth: 0,
      stageHeight: 0,
      zoom: 0,
      centerTileY: 0,
      tileSizePx: 0,
      previewScale: 1,
    }

    const initialEngineData = mapEngine.getInitialData()

    this.setData({
      ...initialEngineData,
      ...buildResolvedSystemSettingsPatch(systemSettingsState),
      showDebugPanel: false,
      showGameInfoPanel: false,
      showResultScene: false,
      showSystemSettingsPanel: false,
      statusBarHeight,
      topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
      hudPanelIndex: 0,
      configSourceText: currentGameLaunchEnvelope.config.configLabel,
      gameInfoTitle: '当前游戏',
      gameInfoSubtitle: '未开始',
      gameInfoLocalRows: [],
      gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
      resultSceneTitle: '本局结果',
      resultSceneSubtitle: '未开始',
      resultSceneHeroLabel: '本局用时',
      resultSceneHeroValue: '--',
      resultSceneRows: buildEmptyResultSceneSnapshot().rows,
      resultSceneCountdownText: '',
      panelTimerText: '00:00:00',
      panelTimerMode: 'elapsed',
      panelTimerFxClass: '',
      panelMileageText: '0m',
      panelMileageFxClass: '',
      panelActionTagText: '目标',
      panelDistanceTagText: '点距',
      panelTargetSummaryText: '等待选择目标',
      panelDistanceValueText: '--',
      panelDistanceUnitText: '',
      panelProgressText: '0/0',
      showPunchHintBanner: true,
      gameSessionStatus: 'idle',
      gameModeText: '顺序赛',
      gpsLockEnabled: false,
      gpsLockAvailable: false,
      locationSourceMode: 'real',
      locationSourceText: '真实定位',
      mockBridgeConnected: false,
      mockBridgeStatusText: '未连接',
      mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
      mockChannelIdText: storedMockChannelId,
      mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
      mockChannelIdDraft: storedMockChannelId,
      mockCoordText: '--',
      mockSpeedText: '--',
      heartRateSourceMode: 'real',
      heartRateSourceText: '真实心率',
      mockHeartRateBridgeConnected: false,
      mockHeartRateBridgeStatusText: '未连接',
      mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
      mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
      mockHeartRateText: '--',
      mockDebugLogBridgeConnected: false,
      mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
      mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
      mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
      panelSpeedValueText: '0',
      panelSpeedFxClass: '',
      panelTelemetryTone: 'blue',
      gpsLogoStatusText: '未配置',
      gpsLogoSourceText: '--',
      panelHeartRateZoneNameText: '--',
      panelHeartRateZoneRangeText: '',
      heartRateConnected: false,
      heartRateStatusText: '心率带未连接',
      heartRateDeviceText: '--',
      panelHeartRateValueText: '--',
      panelHeartRateFxClass: '',
      panelHeartRateUnitText: '',
      panelCaloriesValueText: '0',
      panelCaloriesUnitText: 'kcal',
      panelAverageSpeedValueText: '0',
      panelAverageSpeedUnitText: 'km/h',
      panelAccuracyValueText: '--',
      panelAccuracyUnitText: '',
      deviceHeadingText: '--',
      devicePoseText: '竖持',
      headingConfidenceText: '低',
      accelerometerText: '--',
      gyroscopeText: '--',
      deviceMotionText: '--',
      compassSourceText: '无数据',
      compassTuningProfileText: initialEngineData.compassTuningProfileText || '平衡',
      punchButtonText: '打点',
      punchButtonEnabled: false,
      skipButtonEnabled: false,
      punchHintText: '等待进入检查点范围',
      punchHintFxClass: '',
      punchFeedbackVisible: false,
      punchFeedbackText: '',
      punchFeedbackTone: 'neutral',
      contentCardVisible: false,
      contentCardTemplate: 'story',
      contentCardTitle: '',
      contentCardBody: '',
      contentCardActions: [],
      contentQuizVisible: false,
      contentQuizQuestionText: '',
      contentQuizCountdownText: '',
      contentQuizOptions: [],
      contentQuizFeedbackVisible: false,
      contentQuizFeedbackText: '',
      contentQuizFeedbackTone: 'neutral',
      punchButtonFxClass: '',
      panelProgressFxClass: '',
      panelDistanceFxClass: '',
      punchFeedbackFxClass: '',
      contentCardFxClass: '',
      mapPulseVisible: false,
      mapPulseLeftPx: 0,
      mapPulseTopPx: 0,
      mapPulseFxClass: '',
      stageFxVisible: false,
      stageFxClass: '',
      compassTicks: buildCompassTicks(),
      compassLabels: buildCompassLabels(),
      ...buildSideButtonVisibility('shown'),
      ...buildSideButtonState({
        sideButtonMode: 'shown',
        showGameInfoPanel: false,
        showSystemSettingsPanel: false,
        showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
        centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
        skipButtonEnabled: false,
        gameSessionStatus: 'idle',
        gpsLockEnabled: false,
        gpsLockAvailable: false,
      }),
      ...buildCenterScaleRulerPatch({
        ...(mapEngine.getInitialData() as MapPageData),
        showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
        centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
        stageWidth: 0,
        stageHeight: 0,
        topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
        zoom: 0,
        centerTileY: 0,
        tileSizePx: 0,
      }),
    }, () => {
      if (shouldAutoConnectMockSources) {
        this.handleConnectAllMockSources()
      }
    })
  },

  onReady() {
    stageCanvasAttached = false
    this.measureStageAndCanvas()
    this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
    const app = getApp<IAppOption>()
    const pendingHeartRateAutoConnect = app.globalData ? app.globalData.pendingHeartRateAutoConnect : null
    if (pendingHeartRateAutoConnect && pendingHeartRateAutoConnect.enabled && mapEngine) {
      const pendingDeviceName = pendingHeartRateAutoConnect.deviceName || '心率带'
      app.globalData.pendingHeartRateAutoConnect = null
      mapEngine.handleConnectHeartRate()
      this.setData({
        statusText: `正在自动连接局前设备：${pendingDeviceName}`,
        heartRateStatusText: `正在自动连接 ${pendingDeviceName}`,
        heartRateDeviceText: pendingDeviceName,
      })
    }
  },

  onShow() {
    if (mapEngine) {
      this.applyCompiledRuntimeProfiles()
      mapEngine.handleAppShow()
    }
  },

  onHide() {
    this.persistSessionRecoverySnapshot()
    clearResultExitRedirectTimer()
    clearResultExitCountdownTimer()
    if (mapEngine) {
      mapEngine.handleAppHide()
    }
  },

  onUnload() {
    this.persistSessionRecoverySnapshot()
    clearSessionRecoveryPersistTimer()
    clearResultExitRedirectTimer()
    clearResultExitCountdownTimer()
    syncedBackendSessionStartId = ''
    syncedBackendSessionFinishId = ''
    clearGameInfoPanelSyncTimer()
    clearCenterScaleRulerSyncTimer()
    clearCenterScaleRulerUpdateTimer()
    clearPunchHintDismissTimer()
    clearPunchHintFxTimer()
    clearHudFxTimer('timer')
    clearHudFxTimer('mileage')
    clearHudFxTimer('speed')
    clearHudFxTimer('heartRate')
    if (mapEngine) {
      mapEngine.destroy()
      mapEngine = null
    }
    currentSystemSettingsConfig = undefined
    currentRemoteMapConfig = undefined
    systemSettingsLockLifetimeActive = false
    currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
    shouldAutoRestoreRecoverySnapshot = false
    redirectedToResultPage = false
    stageCanvasAttached = false
  },

  loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
    this.loadMapConfigFromRemote(
      envelope.config.configUrl,
      envelope.config.configLabel,
    )
  },

  persistSessionRecoverySnapshot() {
    if (!mapEngine || !currentRemoteMapConfig) {
      return false
    }

    const runtimeSnapshot = mapEngine.buildSessionRecoveryRuntimeSnapshot()
    if (!runtimeSnapshot) {
      return false
    }

    const snapshot: SessionRecoverySnapshot = {
      schemaVersion: 1,
      savedAt: Date.now(),
      launchEnvelope: currentGameLaunchEnvelope,
      configAppId: currentRemoteMapConfig.configAppId,
      configVersion: currentRemoteMapConfig.configVersion,
      runtime: runtimeSnapshot,
    }
    saveSessionRecoverySnapshot(snapshot)
    return true
  },

  syncBackendSessionStart() {
    const sessionContext = getCurrentBackendSessionContext()
    if (!sessionContext || syncedBackendSessionStartId === sessionContext.sessionId) {
      return
    }

    startSession({
      baseUrl: getCurrentBackendBaseUrl(),
      sessionId: sessionContext.sessionId,
      sessionToken: sessionContext.sessionToken,
    })
      .then(() => {
        syncedBackendSessionStartId = sessionContext.sessionId
      })
      .catch((error) => {
        const message = error && error.message ? error.message : '未知错误'
        this.setData({
          statusText: `session start 上报失败: ${message}`,
        })
      })
  },

  syncBackendSessionFinish(statusOverride?: 'finished' | 'failed' | 'cancelled') {
    const sessionContext = getCurrentBackendSessionContext()
    if (!sessionContext || syncedBackendSessionFinishId === sessionContext.sessionId || !mapEngine) {
      return
    }

    const finishSummary = mapEngine.getSessionFinishSummary(statusOverride)
    if (!finishSummary) {
      return
    }

    const summaryPayload: BackendSessionFinishSummaryPayload = {}
    if (typeof finishSummary.finalDurationSec === 'number') {
      summaryPayload.finalDurationSec = finishSummary.finalDurationSec
    }
    if (typeof finishSummary.finalScore === 'number') {
      summaryPayload.finalScore = finishSummary.finalScore
    }
    if (typeof finishSummary.completedControls === 'number') {
      summaryPayload.completedControls = finishSummary.completedControls
    }
    if (typeof finishSummary.totalControls === 'number') {
      summaryPayload.totalControls = finishSummary.totalControls
    }
    if (typeof finishSummary.distanceMeters === 'number') {
      summaryPayload.distanceMeters = finishSummary.distanceMeters
    }
    if (typeof finishSummary.averageSpeedKmh === 'number') {
      summaryPayload.averageSpeedKmh = finishSummary.averageSpeedKmh
    }

    finishSession({
      baseUrl: getCurrentBackendBaseUrl(),
      sessionId: sessionContext.sessionId,
      sessionToken: sessionContext.sessionToken,
      status: finishSummary.status,
      summary: summaryPayload,
    })
      .then(() => {
        syncedBackendSessionFinishId = sessionContext.sessionId
      })
      .catch((error) => {
        const message = error && error.message ? error.message : '未知错误'
        this.setData({
          statusText: `session finish 上报失败: ${message}`,
        })
      })
  },

  reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
    const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
    if (!sessionContext) {
      clearSessionRecoverySnapshot()
      return
    }

    finishSession({
      baseUrl: getCurrentBackendBaseUrl(),
      sessionId: sessionContext.sessionId,
      sessionToken: sessionContext.sessionToken,
      status: 'cancelled',
      summary: {},
    })
      .then(() => {
        syncedBackendSessionFinishId = sessionContext.sessionId
        clearSessionRecoverySnapshot()
        wx.showToast({
          title: '已放弃上次对局',
          icon: 'none',
          duration: 1400,
        })
      })
      .catch((error) => {
        clearSessionRecoverySnapshot()
        const message = error && error.message ? error.message : '未知错误'
        this.setData({
          statusText: `放弃恢复已生效，后端取消上报失败: ${message}`,
        })
        wx.showToast({
          title: '已放弃上次对局',
          icon: 'none',
          duration: 1400,
        })
      })
  },

  stashPendingResultSnapshot(snapshot: MapEngineResultSnapshot) {
    const app = getApp<IAppOption>()
    if (app.globalData) {
      app.globalData.pendingResultSnapshot = snapshot
      app.globalData.pendingResultLaunchEnvelope = currentGameLaunchEnvelope
    }
  },

  redirectToResultPage() {
    if (redirectedToResultPage) {
      return
    }
    clearResultExitRedirectTimer()
    clearResultExitCountdownTimer()
    redirectedToResultPage = true
    const sessionContext = getCurrentBackendSessionContext()
    const resultUrl = sessionContext
      ? `/pages/result/result?sessionId=${encodeURIComponent(sessionContext.sessionId)}`
      : '/pages/result/result'
    wx.redirectTo({
      url: resultUrl,
    })
  },

  presentResultExitPrompt() {
    clearResultExitRedirectTimer()
    clearResultExitCountdownTimer()

    let remainingSeconds = Math.ceil(RESULT_EXIT_REDIRECT_DELAY_MS / 1000)
    this.setData({
      showResultScene: true,
      resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
    })

    resultExitCountdownTimer = setInterval(() => {
      remainingSeconds -= 1
      if (remainingSeconds <= 0) {
        clearResultExitCountdownTimer()
        return
      }

      this.setData({
        resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
      })
    }, 1000) as unknown as number

    resultExitRedirectTimer = setTimeout(() => {
      resultExitRedirectTimer = 0
      this.redirectToResultPage()
    }, RESULT_EXIT_REDIRECT_DELAY_MS) as unknown as number
  },

  restoreRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
    systemSettingsLockLifetimeActive = true
    this.applyRuntimeSystemSettings(true)
    const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
    if (!restored) {
      clearSessionRecoverySnapshot()
      wx.showToast({
        title: '恢复失败，已回到初始状态',
        icon: 'none',
        duration: 1600,
      })
      return false
    }

    this.setData({
      showResultScene: false,
      showDebugPanel: false,
      showGameInfoPanel: false,
      showSystemSettingsPanel: false,
    })
    const sessionContext = getCurrentBackendSessionContext()
    if (sessionContext) {
      syncedBackendSessionStartId = sessionContext.sessionId
    }
    this.syncSessionRecoveryLifecycle('running')
    return true
  },

  syncSessionRecoveryLifecycle(status: MapPageData['gameSessionStatus']) {
    if (status === 'running') {
      this.persistSessionRecoverySnapshot()
      if (!sessionRecoveryPersistTimer) {
        sessionRecoveryPersistTimer = setInterval(() => {
          this.persistSessionRecoverySnapshot()
        }, SESSION_RECOVERY_PERSIST_INTERVAL_MS) as unknown as number
      }
      return
    }

    clearSessionRecoveryPersistTimer()
  },

  maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
    const snapshot = loadSessionRecoverySnapshot()
    if (!snapshot || !mapEngine) {
      return
    }

    if (
      snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
      || snapshot.configAppId !== config.configAppId
      || snapshot.configVersion !== config.configVersion
    ) {
      clearSessionRecoverySnapshot()
      return
    }

    if (shouldAutoRestoreRecoverySnapshot) {
      shouldAutoRestoreRecoverySnapshot = false
      this.restoreRecoverySnapshot(snapshot)
      return
    }

    wx.showModal({
      title: '恢复对局',
      content: '检测到上次有未正常结束的对局，是否继续恢复？',
      confirmText: '继续恢复',
      cancelText: '放弃',
      success: (result) => {
        if (!result.confirm) {
          this.reportAbandonedRecoverySnapshot(snapshot)
          return
        }

        this.restoreRecoverySnapshot(snapshot)
      },
    })
  },

  compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
    if (!currentRemoteMapConfig) {
      return null
    }

    return compileRuntimeProfile(currentRemoteMapConfig, {
      playerTelemetryProfile: getGlobalTelemetryProfile(),
      settingsLockLifetimeActive: lockLifetimeActive,
    })
  },

  applyCompiledRuntimeProfiles(
    lockLifetimeActive = isSystemSettingsLockLifetimeActive(),
    options?: {
      includeSettings?: boolean
      includeMap?: boolean
      includeGame?: boolean
      includePresentation?: boolean
      includeTelemetry?: boolean
      includeFeedback?: boolean
    },
  ) {
    const currentEngine = mapEngine
    if (!currentEngine) {
      return null
    }

    const compiledProfile = this.compileCurrentRuntimeProfile(lockLifetimeActive)
    if (!compiledProfile) {
      return null
    }

    if (options && options.includeMap) {
      currentEngine.applyCompiledMapProfile(compiledProfile.map)
    }
    if (options && options.includeSettings) {
      currentEngine.applyCompiledSettingsProfile(compiledProfile.settings)
    }
    if (options && options.includeGame) {
      currentEngine.applyCompiledGameProfile(compiledProfile.game)
    }
    if (options && options.includePresentation) {
      currentEngine.applyCompiledPresentationProfile(compiledProfile.presentation)
    }
    if (!options || options.includeTelemetry !== false) {
      currentEngine.applyCompiledTelemetryProfile(compiledProfile.telemetry)
    }
    if (!options || options.includeFeedback !== false) {
      currentEngine.applyCompiledFeedbackProfile(compiledProfile.feedback)
    }
    return compiledProfile
  },

  applyRuntimeSystemSettings(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
    const currentEngine = mapEngine
    if (!currentEngine) {
      return null
    }

    const compiledProfile = this.applyCompiledRuntimeProfiles(lockLifetimeActive, {
      includeSettings: true,
    })
      || {
        settings: resolveSystemSettingsState(
          currentSystemSettingsConfig,
          undefined,
          lockLifetimeActive,
        ),
      }
    const resolvedSettings = compiledProfile.settings

    const engineSnapshot = currentEngine.getInitialData() as Partial<MapPageData>
    updateCenterScaleRulerInputCache(engineSnapshot)
    const resolvedPatch = buildResolvedSystemSettingsPatch(resolvedSettings)
    const mergedData = {
      ...centerScaleRulerInputCache,
      ...this.data,
      ...engineSnapshot,
      ...resolvedPatch,
    } as MapPageData

    this.setData({
      ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, resolvedSettings.values.showCenterScaleRuler),
      ...resolvedPatch,
      ...buildCenterScaleRulerPatch(mergedData),
      ...buildSideButtonState(mergedData),
    })
    return resolvedSettings
  },

  persistAndApplySystemSettings(
    patch: Partial<StoredUserSettings>,
    options?: {
      applyCenterScaleRuler?: boolean
    },
  ) {
    updateStoredUserSettings(patch)
    const lockLifetimeActive = isSystemSettingsLockLifetimeActive()
    const resolvedSettings = this.applyRuntimeSystemSettings(lockLifetimeActive)
    if (!resolvedSettings || !(options && options.applyCenterScaleRuler)) {
      return resolvedSettings
    }

    this.applyCenterScaleRulerSettings(
      resolvedSettings.values.showCenterScaleRuler,
      resolvedSettings.values.centerScaleRulerAnchorMode,
    )
    return resolvedSettings
  },

  loadMapConfigFromRemote(configUrl: string, configLabel: string) {
    const currentEngine = mapEngine
    if (!currentEngine) {
      return
    }

    this.setData({
      configSourceText: configLabel,
      configStatusText: `加载中: ${configLabel}`,
    })

    loadRemoteMapConfig(configUrl)
      .then((config) => {
        if (mapEngine !== currentEngine) {
          return
        }

        currentEngine.applyRemoteMapConfig(config)
        this.applyConfiguredSystemSettings(config)
        this.applyCompiledRuntimeProfiles(true, {
          includeMap: true,
          includeGame: true,
          includePresentation: true,
        })
        this.maybePromptSessionRecoveryRestore(config)
      })
      .catch((error) => {
        if (mapEngine !== currentEngine) {
          return
        }

        const rawErrorMessage = error && error.message ? error.message : '未知错误'
        const errorMessage = rawErrorMessage.indexOf('404') >= 0
          ? `release manifest 不存在或未发布 (${configLabel})`
          : rawErrorMessage
        this.setData({
          configStatusText: `载入失败: ${errorMessage}`,
          statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
        })
      })
  },

  applyConfiguredSystemSettings(config: RemoteMapConfig) {
    currentRemoteMapConfig = config
    currentSystemSettingsConfig = config.systemSettingsConfig
    systemSettingsLockLifetimeActive = true
    this.applyRuntimeSystemSettings(true)
  },

  measureStageAndCanvas(onApplied?: () => void) {
    const page = this
    const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
      const fallbackRect = getFallbackStageRect()
      const rect: MapEngineStageRect = {
        width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
        height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
        left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
        top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
      }

      const currentEngine = mapEngine
      if (!currentEngine) {
        return
      }

      currentEngine.setStage(rect)
      if (onApplied) {
        onApplied()
      }

      if (stageCanvasAttached) {
        return
      }

      const canvasQuery = wx.createSelectorQuery().in(page)
      canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
      canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
      canvasQuery.exec((canvasRes) => {
        const canvasRef = canvasRes[0] as any
        const labelCanvasRef = canvasRes[1] as any
        if (!canvasRef || !canvasRef.node) {
          page.setData({
            statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
          })
          return
        }

        const dpr = wx.getSystemInfoSync().pixelRatio || 1
        try {
          currentEngine.attachCanvas(
            canvasRef.node,
            rect.width,
            rect.height,
            dpr,
            labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
          )
          stageCanvasAttached = true
        } catch (error) {
          page.setData({
            statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
          })
        }
      })
    }

    const query = wx.createSelectorQuery().in(page)
    query.select('.map-stage').boundingClientRect()
    query.exec((res) => {
      const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
      applyStage(rect)
    })
  },

  handleTouchStart(event: WechatMiniprogram.TouchEvent) {
    if (mapEngine) {
      mapEngine.handleTouchStart(event)
    }
  },

  handleTouchMove(event: WechatMiniprogram.TouchEvent) {
    if (mapEngine) {
      mapEngine.handleTouchMove(event)
    }
  },

  handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
    if (mapEngine) {
      mapEngine.handleTouchEnd(event)
    }
  },

  handleTouchCancel() {
    if (mapEngine) {
      mapEngine.handleTouchCancel()
    }
  },

  handleRecenter() {
    if (mapEngine) {
      mapEngine.handleRecenter()
    }
  },

  handleRotateStep() {
    if (mapEngine) {
      mapEngine.handleRotateStep()
    }
  },

  handleRotationReset() {
    if (mapEngine) {
      mapEngine.handleRotationReset()
    }
  },

  handleSetManualMode() {
    if (mapEngine) {
      mapEngine.handleSetManualMode()
    }
  },

  handleSetNorthUpMode() {
    if (mapEngine) {
      mapEngine.handleSetNorthUpMode()
    }
  },

  handleSetHeadingUpMode() {
    if (mapEngine) {
      mapEngine.handleSetHeadingUpMode()
    }
  },

  handleCycleNorthReferenceMode() {
    if (mapEngine) {
      mapEngine.handleCycleNorthReferenceMode()
    }
  },

  handleAutoRotateCalibrate() {
    if (mapEngine) {
      mapEngine.handleAutoRotateCalibrate()
    }
  },

  handleToggleGpsTracking() {
    if (mapEngine) {
      mapEngine.handleToggleGpsTracking()
    }
  },

  handleSetRealLocationMode() {
    if (mapEngine) {
      mapEngine.handleSetRealLocationMode()
    }
  },

  handleSetMockLocationMode() {
    if (mapEngine) {
      mapEngine.handleSetMockLocationMode()
    }
  },

  handleConnectMockLocationBridge() {
    if (mapEngine) {
      mapEngine.handleConnectMockLocationBridge()
    }
  },

  handleConnectAllMockSources() {
    if (!mapEngine) {
      return
    }
    const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
    this.setData({
      mockChannelIdDraft: channelId,
    })
    persistMockChannelId(channelId)
    persistMockAutoConnectEnabled(true)
    mapEngine.handleSetMockChannelId(channelId)
    mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
    mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
    mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
    mapEngine.handleConnectMockLocationBridge()
    mapEngine.handleSetMockLocationMode()
    mapEngine.handleSetMockHeartRateMode()
    mapEngine.handleConnectMockHeartRateBridge()
    mapEngine.handleConnectMockDebugLogBridge()
  },

  handleOpenWebViewTest() {
    wx.navigateTo({
      url: '/pages/webview-test/webview-test',
    })
  },

  handleMockChannelIdInput(event: WechatMiniprogram.Input) {
    this.setData({
      mockChannelIdDraft: event.detail.value,
    })
  },

  handleSaveMockChannelId() {
    const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
    this.setData({
      mockChannelIdDraft: channelId,
    })
    persistMockChannelId(channelId)
    if (mapEngine) {
      mapEngine.handleSetMockChannelId(channelId)
    }
  },

  handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
    this.setData({
      mockBridgeUrlDraft: event.detail.value,
    })
  },

  handleSaveMockBridgeUrl() {
    if (mapEngine) {
      mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
    }
  },

  handleDisconnectMockLocationBridge() {
    persistMockAutoConnectEnabled(false)
    if (mapEngine) {
      mapEngine.handleDisconnectMockLocationBridge()
    }
  },

  handleSetRealHeartRateMode() {
    if (mapEngine) {
      mapEngine.handleSetRealHeartRateMode()
    }
  },

  handleSetMockHeartRateMode() {
    if (mapEngine) {
      mapEngine.handleSetMockHeartRateMode()
    }
  },

  handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
    this.setData({
      mockHeartRateBridgeUrlDraft: event.detail.value,
    })
  },

  handleSaveMockHeartRateBridgeUrl() {
    if (mapEngine) {
      mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
    }
  },

  handleMockDebugLogBridgeUrlInput(event: WechatMiniprogram.Input) {
    this.setData({
      mockDebugLogBridgeUrlDraft: event.detail.value,
    })
  },

  handleSaveMockDebugLogBridgeUrl() {
    if (mapEngine) {
      mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
    }
  },

  handleConnectMockDebugLogBridge() {
    if (mapEngine) {
      mapEngine.handleConnectMockDebugLogBridge()
    }
  },

  handleDisconnectMockDebugLogBridge() {
    persistMockAutoConnectEnabled(false)
    if (mapEngine) {
      mapEngine.handleDisconnectMockDebugLogBridge()
    }
  },

  handleConnectMockHeartRateBridge() {
    if (mapEngine) {
      mapEngine.handleConnectMockHeartRateBridge()
    }
  },

  handleDisconnectMockHeartRateBridge() {
    persistMockAutoConnectEnabled(false)
    if (mapEngine) {
      mapEngine.handleDisconnectMockHeartRateBridge()
    }
  },

  handleConnectHeartRate() {
      if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
        return
      }
      if (mapEngine) {
        mapEngine.handleConnectHeartRate()
      }
    },

    handleOpenHeartRateDevicePicker() {
      if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
        return
      }
      this.setData({
        showHeartRateDevicePicker: true,
      })
      if (mapEngine) {
        mapEngine.handleConnectHeartRate()
      }
    },

    handleCloseHeartRateDevicePicker() {
      this.setData({
        showHeartRateDevicePicker: false,
      })
    },

      handleDisconnectHeartRate() {
        if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
          return
        }
        if (mapEngine) {
          mapEngine.handleDisconnectHeartRate()
        }
    },

    handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
      if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
        const targetDeviceId = event.currentTarget.dataset.deviceId
        const targetDevice = this.data.heartRateDiscoveredDevices.find((item) => item.deviceId === targetDeviceId)
        pendingHeartRateSwitchDeviceName = targetDevice ? targetDevice.name : null
        mapEngine.handleConnectHeartRateDevice(targetDeviceId)
        this.setData({
          showHeartRateDevicePicker: false,
          statusText: targetDevice
            ? `正在切换到 ${targetDevice.name}`
            : '正在切换心率带设备',
        })
      }
    },

  handleClearPreferredHeartRateDevice() {
    if (this.data.lockHeartRateDevice) {
      return
    }
    if (mapEngine) {
      mapEngine.handleClearPreferredHeartRateDevice()
    }
  },

  handleDebugHeartRateBlue() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('blue')
    }
  },

  handleDebugHeartRatePurple() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('purple')
    }
  },

  handleDebugHeartRateGreen() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('green')
    }
  },

  handleDebugHeartRateYellow() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('yellow')
    }
  },

  handleDebugHeartRateOrange() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('orange')
    }
  },

  handleDebugHeartRateRed() {
    if (mapEngine) {
      mapEngine.handleDebugHeartRateTone('red')
    }
  },

  handleDebugSetSessionRemainingWarning() {
    if (mapEngine) {
      mapEngine.handleDebugSetSessionRemainingWarning()
    }
  },

  handleDebugSetSessionRemainingOneMinute() {
    if (mapEngine) {
      mapEngine.handleDebugSetSessionRemainingOneMinute()
    }
  },

  handleDebugTimeoutSession() {
    if (mapEngine) {
      mapEngine.handleDebugTimeoutSession()
    }
  },

  handleClearDebugHeartRate() {
    if (mapEngine) {
      mapEngine.handleClearDebugHeartRate()
    }
  },

  handleToggleOsmReference() {
    if (mapEngine) {
      mapEngine.handleToggleOsmReference()
    }
  },

  handleStartGame() {
    if (mapEngine) {
      systemSettingsLockLifetimeActive = true
      this.applyRuntimeSystemSettings(true)
      mapEngine.handleStartGame()
    }
  },

  handleLoadClassicConfig() {
    currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('classic')
    this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  },

  handleLoadScoreOConfig() {
    currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('score-o')
    this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  },

  handleForceExitGame() {
    if (!mapEngine || this.data.gameSessionStatus !== 'running') {
      return
    }

      wx.showModal({
        title: '确认退出',
        content: '确认强制结束当前对局并返回开始前状态？',
        confirmText: '确认退出',
        cancelText: '取消',
        success: (result) => {
          if (result.confirm && mapEngine) {
            clearResultExitRedirectTimer()
            clearResultExitCountdownTimer()
            this.syncBackendSessionFinish('cancelled')
            clearSessionRecoverySnapshot()
            clearSessionRecoveryPersistTimer()
            systemSettingsLockLifetimeActive = false
            mapEngine.handleForceExitGame()
            wx.showToast({
              title: '已退出当前对局',
              icon: 'none',
              duration: 1000,
            })
            setTimeout(() => {
              navigateAwayFromMapAfterCancel()
            }, 180)
          }
        },
      })
  },

  handleSkipAction() {
    if (!mapEngine || !this.data.skipButtonEnabled) {
      return
    }

    if (!mapEngine.shouldConfirmSkipAction()) {
      mapEngine.handleSkipAction()
      return
    }

    wx.showModal({
      title: '确认跳点',
      content: '确认跳过当前检查点并切换到下一个目标点？',
      confirmText: '确认跳过',
      cancelText: '取消',
      success: (result) => {
        if (result.confirm && mapEngine) {
          mapEngine.handleSkipAction()
        }
      },
    })
  },

  handleClearMapTestArtifacts() {
    if (mapEngine) {
      mapEngine.handleClearMapTestArtifacts()
    }
  },

  syncGameInfoPanelSnapshot() {
    if (!mapEngine) {
      return
    }

    const snapshot = mapEngine.getGameInfoSnapshot()
    const localRows = snapshot.localRows.concat([
      ...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
      { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
      { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
      { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
      { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
      { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
      { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
      { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
      { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
    ])
    this.setData({
      gameInfoTitle: snapshot.title,
      gameInfoSubtitle: snapshot.subtitle,
      gameInfoLocalRows: localRows,
      gameInfoGlobalRows: snapshot.globalRows,
    })
  },

  syncResultSceneSnapshot() {
    if (!mapEngine) {
      return
    }

    const snapshot = mapEngine.getResultSceneSnapshot()
    this.setData({
      resultSceneTitle: snapshot.title,
      resultSceneSubtitle: snapshot.subtitle,
      resultSceneHeroLabel: snapshot.heroLabel,
      resultSceneHeroValue: snapshot.heroValue,
      resultSceneRows: snapshot.rows.concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope)),
    })
  },

  scheduleGameInfoPanelSnapshotSync() {
    if (!this.data.showGameInfoPanel) {
      clearGameInfoPanelSyncTimer()
      return
    }

    if (gameInfoPanelSyncTimer) {
      return
    }

    gameInfoPanelSyncTimer = setTimeout(() => {
      gameInfoPanelSyncTimer = 0
      if (this.data.showGameInfoPanel) {
        this.syncGameInfoPanelSnapshot()
      }
    }, 400) as unknown as number
  },

  handleOpenGameInfoPanel() {
    clearGameInfoPanelSyncTimer()
    this.syncGameInfoPanelSnapshot()
    this.setData({
      showDebugPanel: false,
      showSystemSettingsPanel: false,
      showGameInfoPanel: true,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: true,
        showSystemSettingsPanel: false,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  handleCloseGameInfoPanel() {
    clearGameInfoPanelSyncTimer()
    this.setData({
      showGameInfoPanel: false,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: false,
        showSystemSettingsPanel: this.data.showSystemSettingsPanel,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  handleGameInfoPanelTap() {},

  handleResultSceneTap() {},

  handleCloseResultScene() {
    this.redirectToResultPage()
  },

  handleRestartFromResult() {
    this.redirectToResultPage()
  },

  handleOpenSystemSettingsPanel() {
    clearGameInfoPanelSyncTimer()
    this.setData({
      showDebugPanel: false,
      showGameInfoPanel: false,
      showSystemSettingsPanel: true,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: false,
        showSystemSettingsPanel: true,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  handleCloseSystemSettingsPanel() {
    this.setData({
      showSystemSettingsPanel: false,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: this.data.showGameInfoPanel,
        showSystemSettingsPanel: false,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  handleSystemSettingsPanelTap() {},

  handleSetAnimationLevelStandard() {
    if (this.data.lockAnimationLevel || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      animationLevel: 'standard',
    })
  },

  handleSetAnimationLevelLite() {
    if (this.data.lockAnimationLevel || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      animationLevel: 'lite',
    })
  },

  handleSetTrackModeNone() {
    if (this.data.lockTrackMode || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackDisplayMode: 'none',
    })
  },

  handleSetTrackModeTail() {
    if (this.data.lockTrackMode || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackDisplayMode: 'tail',
    })
  },

  handleSetTrackModeFull() {
    if (this.data.lockTrackMode || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackDisplayMode: 'full',
    })
  },

  handleSetTrackTailLengthShort() {
    if (this.data.lockTrackTailLength || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackTailLength: 'short',
    })
  },

  handleSetTrackTailLengthMedium() {
    if (this.data.lockTrackTailLength || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackTailLength: 'medium',
    })
  },

  handleSetTrackTailLengthLong() {
    if (this.data.lockTrackTailLength || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackTailLength: 'long',
    })
  },

  handleSetTrackColorPreset(event: WechatMiniprogram.TouchEvent) {
    if (this.data.lockTrackColor || !mapEngine) {
      return
    }
    const color = event.currentTarget.dataset.color as TrackColorPreset | undefined
    if (!color) {
      return
    }
    this.persistAndApplySystemSettings({
      trackColorPreset: color,
    })
  },

  handleSetTrackStyleClassic() {
    if (this.data.lockTrackStyle || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackStyleProfile: 'classic',
    })
  },

  handleSetTrackStyleNeon() {
    if (this.data.lockTrackStyle || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      trackStyleProfile: 'neon',
    })
  },

  handleSetGpsMarkerVisibleOn() {
    if (this.data.lockGpsMarkerVisible || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      gpsMarkerVisible: true,
    })
  },

  handleSetGpsMarkerVisibleOff() {
      if (this.data.lockGpsMarkerVisible || !mapEngine) {
        return
      }
    this.persistAndApplySystemSettings({
        gpsMarkerVisible: false,
      })
    },

    handleSetGpsMarkerStyleDot() {
      if (this.data.lockGpsMarkerStyle || !mapEngine) {
        return
      }
      this.persistAndApplySystemSettings({
        gpsMarkerStyle: 'dot',
      })
    },

    handleSetGpsMarkerStyleBeacon() {
      if (this.data.lockGpsMarkerStyle || !mapEngine) {
        return
      }
      this.persistAndApplySystemSettings({
        gpsMarkerStyle: 'beacon',
      })
    },

    handleSetGpsMarkerStyleDisc() {
      if (this.data.lockGpsMarkerStyle || !mapEngine) {
        return
      }
      this.persistAndApplySystemSettings({
        gpsMarkerStyle: 'disc',
      })
    },

    handleSetGpsMarkerStyleBadge() {
      if (this.data.lockGpsMarkerStyle || !mapEngine) {
        return
      }
      this.persistAndApplySystemSettings({
        gpsMarkerStyle: 'badge',
      })
    },

  handleSetGpsMarkerSizeSmall() {
      if (this.data.lockGpsMarkerSize || !mapEngine) {
        return
    }
    this.persistAndApplySystemSettings({
      gpsMarkerSize: 'small',
    })
  },

  handleSetGpsMarkerSizeMedium() {
    if (this.data.lockGpsMarkerSize || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      gpsMarkerSize: 'medium',
    })
  },

  handleSetGpsMarkerSizeLarge() {
    if (this.data.lockGpsMarkerSize || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      gpsMarkerSize: 'large',
    })
  },

  handleSetGpsMarkerColorPreset(event: WechatMiniprogram.TouchEvent) {
    if (this.data.lockGpsMarkerColor || !mapEngine) {
      return
    }
    const color = event.currentTarget.dataset.color as GpsMarkerColorPreset | undefined
    if (!color) {
      return
    }
    this.persistAndApplySystemSettings({
      gpsMarkerColorPreset: color,
    })
  },

  handleSetSideButtonPlacementLeft() {
    if (this.data.lockSideButtonPlacement) {
      return
    }
    this.persistAndApplySystemSettings({
      sideButtonPlacement: 'left',
    })
  },

  handleSetSideButtonPlacementRight() {
    if (this.data.lockSideButtonPlacement) {
      return
    }
    this.persistAndApplySystemSettings({
      sideButtonPlacement: 'right',
    })
  },

  handleSetAutoRotateEnabledOn() {
    if (this.data.lockAutoRotate || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      autoRotateEnabled: true,
    })
  },

  handleSetAutoRotateEnabledOff() {
    if (this.data.lockAutoRotate || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      autoRotateEnabled: false,
    })
  },

  handleSetCompassTuningSmooth() {
    if (this.data.lockCompassTuning || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      compassTuningProfile: 'smooth',
    })
  },

  handleSetCompassTuningBalanced() {
    if (this.data.lockCompassTuning || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      compassTuningProfile: 'balanced',
    })
  },

  handleSetCompassTuningResponsive() {
    if (this.data.lockCompassTuning || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      compassTuningProfile: 'responsive',
    })
  },

  handleSetNorthReferenceMagnetic() {
    if (this.data.lockNorthReference || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      northReferenceMode: 'magnetic',
    })
  },

  handleSetNorthReferenceTrue() {
    if (this.data.lockNorthReference || !mapEngine) {
      return
    }
    this.persistAndApplySystemSettings({
      northReferenceMode: 'true',
    })
  },

  handleOverlayTouch() {},

  handlePunchAction() {
    if (!this.data.punchButtonEnabled) {
      return
    }

    if (mapEngine) {
      mapEngine.handlePunchAction()
    }
  },

  handleOpenPendingContentCard() {
    if (mapEngine) {
      mapEngine.openPendingContentCard()
    }
  },

  handleOpenContentCardAction(event: WechatMiniprogram.BaseEvent) {
    if (!mapEngine) {
      return
    }
    wx.showToast({
      title: '点击CTA',
      icon: 'none',
      duration: 900,
    })
    const actionType = event.currentTarget.dataset.type
    const action = typeof actionType === 'string' ? mapEngine.openCurrentContentCardAction(actionType) : null
    if (action === 'detail') {
      wx.showToast({
        title: '打开详情',
        icon: 'none',
        duration: 900,
      })
      return
    }
    if (action === 'quiz') {
      return
    }
    if (action === 'photo') {
      wx.chooseMedia({
        count: 1,
        mediaType: ['image'],
        sourceType: ['camera'],
        success: () => {
          if (mapEngine) {
            mapEngine.handleContentCardPhotoCaptured()
          }
        },
      })
      return
    }
    if (action === 'audio') {
      if (!contentAudioRecorder) {
        contentAudioRecorder = wx.getRecorderManager()
        contentAudioRecorder.onStop(() => {
          contentAudioRecording = false
          if (mapEngine) {
            mapEngine.handleContentCardAudioRecorded()
          }
        })
      }
      const recorder = contentAudioRecorder
      if (!contentAudioRecording) {
        contentAudioRecording = true
        recorder.start({
          duration: 8000,
          format: 'mp3',
        } as any)
        wx.showToast({
          title: '开始录音',
          icon: 'none',
          duration: 800,
        })
      } else {
        recorder.stop()
      }
    }
  },

  handleContentQuizAnswer(event: WechatMiniprogram.BaseEvent) {
    if (!mapEngine) {
      return
    }
    const optionKey = event.currentTarget.dataset.key
    if (typeof optionKey === 'string') {
      mapEngine.handleContentCardQuizAnswer(optionKey)
    }
  },

  handleDismissTransientContentCard() {
    if (mapEngine) {
      mapEngine.closeContentCard()
    }
  },

  handleContentCardTap() {
    if (!mapEngine) {
      return
    }
    if (!this.data.contentCardActions.length) {
      mapEngine.closeContentCard()
    }
  },

  openH5Experience(request: H5ExperienceRequest) {
    wx.navigateTo({
      url: '/pages/experience-webview/experience-webview',
      success: (result) => {
        const eventChannel = result.eventChannel
        eventChannel.on('fallback', (payload: H5ExperienceFallbackPayload) => {
          if (mapEngine) {
            mapEngine.handleH5ExperienceFallback(payload)
          }
        })
        eventChannel.on('close', () => {
          if (mapEngine) {
            mapEngine.handleH5ExperienceClosed()
          }
        })
        eventChannel.on('submitResult', () => {
          if (mapEngine) {
            mapEngine.handleH5ExperienceClosed()
          }
        })
        eventChannel.emit('init', request)
      },
      fail: () => {
        if (mapEngine) {
          mapEngine.handleH5ExperienceFallback(request.fallback)
        }
      },
    })
  },

  handleCloseContentCard() {
    if (mapEngine) {
      mapEngine.closeContentCard()
    }
  },

  handleClosePunchHint() {
    clearPunchHintDismissTimer()
    this.setData({
      showPunchHintBanner: false,
    })
  },

  handlePunchHintTap() {},

  handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
    this.setData({
      hudPanelIndex: event.detail.current || 0,
    })
  },

  handleCycleSideButtons() {
    const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
    this.setData({
      ...buildSideButtonVisibility(nextMode),
      ...buildSideButtonState({
        sideButtonMode: nextMode,
        showGameInfoPanel: this.data.showGameInfoPanel,
        showSystemSettingsPanel: this.data.showSystemSettingsPanel,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },
  handleToggleGpsLock() {
    if (mapEngine) {
      mapEngine.handleToggleGpsLock()
    }
  },
  handleToggleMapRotateMode() {
    if (!mapEngine || this.data.lockAutoRotate) {
      return
    }

    if (this.data.orientationMode === 'heading-up') {
      this.persistAndApplySystemSettings({
        autoRotateEnabled: false,
      })
      return
    }

    this.persistAndApplySystemSettings({
      autoRotateEnabled: true,
    })
  },
  handleToggleDebugPanel() {
    const nextShowDebugPanel = !this.data.showDebugPanel
    if (!nextShowDebugPanel) {
      clearGameInfoPanelSyncTimer()
    }
    if (mapEngine) {
      mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
    }
    this.setData({
      showDebugPanel: nextShowDebugPanel,
      showGameInfoPanel: false,
      showSystemSettingsPanel: false,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: false,
        showSystemSettingsPanel: false,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  handleCloseDebugPanel() {
    if (mapEngine) {
      mapEngine.setDiagnosticUiEnabled(false)
    }
    this.setData({
      showDebugPanel: false,
      ...buildSideButtonState({
        sideButtonMode: this.data.sideButtonMode,
        showGameInfoPanel: this.data.showGameInfoPanel,
        showSystemSettingsPanel: this.data.showSystemSettingsPanel,
        showCenterScaleRuler: this.data.showCenterScaleRuler,
        centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
        skipButtonEnabled: this.data.skipButtonEnabled,
        gameSessionStatus: this.data.gameSessionStatus,
        gpsLockEnabled: this.data.gpsLockEnabled,
        gpsLockAvailable: this.data.gpsLockAvailable,
      }),
    })
  },

  applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
    this.data.showCenterScaleRuler = nextEnabled
    this.data.centerScaleRulerAnchorMode = nextAnchorMode
    clearCenterScaleRulerSyncTimer()
    clearCenterScaleRulerUpdateTimer()

    const syncRulerFromEngine = () => {
      if (!mapEngine) {
        return
      }
      const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
      updateCenterScaleRulerInputCache(engineSnapshot)
      const mergedData = {
        ...centerScaleRulerInputCache,
        ...this.data,
        showCenterScaleRuler: nextEnabled,
        centerScaleRulerAnchorMode: nextAnchorMode,
      } as MapPageData

      this.setData({
        ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
        showCenterScaleRuler: nextEnabled,
        centerScaleRulerAnchorMode: nextAnchorMode,
        ...buildCenterScaleRulerPatch(mergedData),
        ...buildSideButtonState(mergedData),
      })
    }

    if (!nextEnabled) {
      syncRulerFromEngine()
      return
    }

    this.setData({
      showCenterScaleRuler: true,
      centerScaleRulerAnchorMode: nextAnchorMode,
      ...buildSideButtonState({
        ...this.data,
        showCenterScaleRuler: true,
        centerScaleRulerAnchorMode: nextAnchorMode,
      } as MapPageData),
    })

    this.measureStageAndCanvas(() => {
      syncRulerFromEngine()
    })

    centerScaleRulerSyncTimer = setTimeout(() => {
      centerScaleRulerSyncTimer = 0
      if (!this.data.showCenterScaleRuler) {
        return
      }
      syncRulerFromEngine()
    }, 96) as unknown as number
  },

  handleSetCenterScaleRulerVisibleOn() {
    if (this.data.lockScaleRulerVisible) {
      return
    }
    this.persistAndApplySystemSettings({
      showCenterScaleRuler: true,
      centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
    }, {
      applyCenterScaleRuler: true,
    })
  },

  handleSetCenterScaleRulerVisibleOff() {
    if (this.data.lockScaleRulerVisible) {
      return
    }
    this.persistAndApplySystemSettings({
      showCenterScaleRuler: false,
      centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
    }, {
      applyCenterScaleRuler: true,
    })
  },

  handleSetCenterScaleRulerAnchorScreenCenter() {
    if (this.data.lockScaleRulerAnchor) {
      return
    }
    this.persistAndApplySystemSettings({
      showCenterScaleRuler: this.data.showCenterScaleRuler,
      centerScaleRulerAnchorMode: 'screen-center',
    }, {
      applyCenterScaleRuler: true,
    })
  },

  handleSetCenterScaleRulerAnchorCompassCenter() {
    if (this.data.lockScaleRulerAnchor) {
      return
    }
    this.persistAndApplySystemSettings({
      showCenterScaleRuler: this.data.showCenterScaleRuler,
      centerScaleRulerAnchorMode: 'compass-center',
    }, {
      applyCenterScaleRuler: true,
    })
  },

  handleToggleCenterScaleRulerAnchor() {
    if (!this.data.showCenterScaleRuler || this.data.lockScaleRulerAnchor) {
      return
    }

    const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
      ? 'compass-center'
      : 'screen-center'
    this.persistAndApplySystemSettings({
      centerScaleRulerAnchorMode: nextAnchorMode,
      showCenterScaleRuler: this.data.showCenterScaleRuler,
    }, {
      applyCenterScaleRuler: true,
    })
  },

  handleDebugPanelTap() {},
})














































