package com.example.cmpt362_group30_maze_game.game

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.util.Log
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider
import com.example.cmpt362_group30_maze_game.Dialogs
import com.example.cmpt362_group30_maze_game.R
import com.example.cmpt362_group30_maze_game.SettingsActivity
import com.yoimerdr.android.virtualjoystick.control.Control
import com.yoimerdr.android.virtualjoystick.views.JoystickView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin

class GameActivity : AppCompatActivity() {

    companion object {
        const val DIFFICULTY_ARG_KEY = "difficulty"
    }

    lateinit var playerViewModel: PlayerViewModel
    lateinit var mapViewModel: MapDataViewModel
    private var mediaPlayer: MediaPlayer? = null

    private lateinit var gameView: GameSurfaceView
    private lateinit var pauseButton: ImageButton
    private lateinit var timerText: TextView
    private lateinit var gameLogicViewModel: GameLogicViewModel
    private lateinit var settingsSharedPref: SharedPreferences
    private var isManualControl: Boolean = true
    private var difficulty = 0

    // Vibrator used for feedback
    private var vibrator: Vibrator? = null

    // Used to keep a timer for how long the player takes
    private val timerHandler = Handler(Looper.getMainLooper())
    private lateinit var timerRunnable: Runnable

    // Used for manual input movement (joystick)
    private var playerDirection: Control.Direction = Control.Direction.NONE
    private lateinit var joystickView: JoystickView

    // Tilt + slide motion controllers
    private lateinit var motionController: MotionController
    private lateinit var tiltController: TiltController

    // --- Motion tuning: accel -> position (more sensitive to small pushes) ---
    // Movement only from sliding forward/back while phone is flat on the table.
    private val accelDeadband = 0.03f      // smaller: notice smaller accelerations
    private val minStep = 0.0004f          // allow smaller movements to count
    private val baseStep = 0.0035f         // base movement once you push
    private val stepGain = 0.05f           // stronger response to acceleration
    private val maxStep = 0.012f           // slightly higher cap per frame
    private var lastStep = 0f              // for smoothing
    private val stepSmoothFactor = 0.3f    // a bit less smoothing (more responsive)

    // Track last orientation so we can:
    //  1) enforce "phone is roughly flat"
    //  2) avoid moving while you are actively tilting the device
    private var lastPitchDeg = 0f
    private var lastRollDeg = 0f
    private var firstOrientationSample = true
    private val tiltDeltaThreshold = 4.0f   // a bit more forgiving while sliding

    // Short lock after turning so the sub doesn't move mid-snap
    private var turnLockUntil: Long = 0L
    private val turnLockDurationMs: Long = 200L

    // Prevent multiple game-over/victory dialogs (which can crash the app)
    private var gameEnded = false

    @RequiresApi(Build.VERSION_CODES.R)
    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_game)

        // Change colour of android status bar icons so they show on white
        WindowCompat.getInsetsController(window, window.decorView)
            .isAppearanceLightStatusBars = true

        // Resolve vibrator once
        vibrator = resolveVibrator()

        difficulty = intent.getIntExtra(DIFFICULTY_ARG_KEY, 0)

        mapViewModel = ViewModelProvider(this)[MapDataViewModel::class.java]

        // Define map Object and store in view model only on first startup
        if (savedInstanceState == null) {
            when (difficulty) {
                0 -> {
                    mapViewModel.loadData(
                        this,
                        getDrawableUri(resources.getString(R.string.easy_map))
                    )
                    mapViewModel.mapBackground.setObjectScale(3.5f, 3.5f, 3.5f)
                    mapViewModel.mapBackground.displace(3.5f, 3.5f, 0.0f)
                }
                1 -> {
                    mapViewModel.loadData(
                        this,
                        getDrawableUri(resources.getString(R.string.medium_map))
                    )
                    mapViewModel.mapBackground.setObjectScale(6.5f, 6.0f, 6.0f)
                    mapViewModel.mapBackground.displace(6.5f, 6.0f, 0.0f)
                }
                2 -> {
                    mapViewModel.loadData(
                        this,
                        getDrawableUri(resources.getString(R.string.hard_map))
                    )
                    mapViewModel.mapBackground.setObjectScale(12.5f, 10.5f, 10.5f)
                    mapViewModel.mapBackground.displace(12.5f, 10.5f, 0.0f)
                }
                else -> {
                    Log.d("debug", "Invalid difficulty passed to GameActivity")
                    mapViewModel.loadData(this, getDrawableUri("map1"))
                    mapViewModel.mapBackground.setObjectScale(3.5f, 3.5f, 3.5f)
                    mapViewModel.mapBackground.displace(3.5f, 3.5f, 0.0f)
                }
            }
        }

        playerViewModel = ViewModelProvider(this)[PlayerViewModel::class.java]

        // Define Player Object and store in view model only on first startup
        if (savedInstanceState == null) {
            playerViewModel.loadData(this, getDrawableUri("submarine"))

            // Resize model and position at the start of the maze
            playerViewModel.playerObject!!.setObjectScale(0.03f, 0.03f, 0.03f)
            playerViewModel.playerObject!!.displace(
                mapViewModel.mazeStartX,
                mapViewModel.mazeStartY,
                0.0f
            )
            // Sub always nose-up on screen (for inertial mode)
            playerViewModel.playerObject!!.zRotate = 180.0f
        }

        // Define game logic view model
        gameLogicViewModel = ViewModelProvider(this)[GameLogicViewModel::class.java]

        // Game not ended; pause state depends on control mode (set below)
        gameEnded = false

        // Gets the user's settings (MUST be before gameLoop uses it)
        settingsSharedPref = getSharedPreferences(SettingsActivity.PREFS_NAME, MODE_PRIVATE)

        // Manual vs inertial (tilt) mode
        isManualControl = settingsSharedPref.getBoolean(SettingsActivity.KEY_MANUAL, true)

        // Logic to do when the game first starts up
        if (!gameLogicViewModel.gameStarted) {
            gameLogicViewModel.gameStarted = true

            if (!isManualControl) {
                // Inertial / tilt mode:
                // start the round PAUSED, show instructions dialog
                gameLogicViewModel.isPaused = true

                val dialog = Dialogs()
                val bundle = Bundle()
                bundle.putInt(Dialogs.DIALOG_ARG_KEY, Dialogs.TILT_INTRO)
                dialog.arguments = bundle

                dialog.dialogDismissedCallback = object : Dialogs.DialogDismissListener {
                    // Unpause/start game logic ONLY when the user closes the instructions dialog
                    override fun onDialogDismissed() {
                        gameLogicViewModel.isPaused = false
                    }
                }

                dialog.show(supportFragmentManager, Dialogs.TILT_INTRO_TAG)
            }
        }

        // Reattach instruction dialog onDismiss callback if needed (e.g., after rotation)
        val existingInstructionDialog =
            supportFragmentManager.findFragmentByTag(Dialogs.TILT_INTRO_TAG) as Dialogs?
        if (existingInstructionDialog != null) {
            existingInstructionDialog.dialogDismissedCallback = object : Dialogs.DialogDismissListener {
                override fun onDialogDismissed() {
                    gameLogicViewModel.isPaused = false
                }
            }
        }

        // Initialize tilt + sliding motion controllers (MUST be before gameLoop)
        motionController = MotionController(this)
        tiltController = TiltController(this)

        // When the phone yaw snaps 90°, briefly lock forward motion (no translation)
        tiltController.onTurn = {
            turnLockUntil = System.currentTimeMillis() + turnLockDurationMs
        }

        // Pass gameRenderer the player view model and the map data view model
        gameView = findViewById(R.id.game_view)
        gameView.gameRenderer.playerViewModel = playerViewModel
        gameView.gameRenderer.mapDataViewModel = mapViewModel

        // Prepare for rendering
        gameView.initRendering()

        // Begin rendering (start AFTER controllers & prefs are ready)
        gameLogicViewModel.gameLoopJob = CoroutineScope(Dispatchers.Default).launch {
            gameLoop()
        }

        // Adds onclick listener to pause button
        pauseButton = findViewById(R.id.pause_button)
        pauseButton.setOnClickListener {
            val dialog = Dialogs()
            val bundle = Bundle()
            bundle.putInt(Dialogs.DIALOG_ARG_KEY, Dialogs.PAUSE)
            dialog.arguments = bundle

            // Pauses game logic while the pause dialog is up
            gameLogicViewModel.isPaused = true

            dialog.dialogDismissedCallback = object : Dialogs.DialogDismissListener {
                // Unpause game logic ONLY when the user taps "Resume"
                override fun onDialogDismissed() {
                    gameLogicViewModel.isPaused = false
                }
            }

            dialog.show(supportFragmentManager, Dialogs.PAUSE_DIALOG_TAG)
        }

        // Runs the timer to see how long the user takes
        timerText = findViewById(R.id.timer_text)
        updateTimerText()
        timerRunnable = Runnable {
            if (gameLogicViewModel.gameLoopJob?.isActive == true && !gameLogicViewModel.isPaused) {
                gameLogicViewModel.secondsElapsed++
            }
            updateTimerText()
            timerHandler.postDelayed(timerRunnable, 1000)
        }
        timerHandler.postDelayed(timerRunnable, 1000)

        // If enabled, turn on manual input, otherwise turn joystick invisible
        joystickView = findViewById(R.id.joystick)
        if (isManualControl) {
            joystickView.setMoveListener { direction ->
                playerDirection = direction
            }

            joystickView.setMoveEndListener {
                playerDirection = Control.Direction.NONE
            }
        } else {
            joystickView.visibility = View.GONE
        }

        // Set up audio system and begin playing audio
        if (settingsSharedPref.getBoolean(SettingsActivity.KEY_SOUND, true)) {
            val attribution: Context = createAttributionContext("audioAttribution")
            mediaPlayer = MediaPlayer.create(attribution, R.raw.fraction_beep)
            gameLogicViewModel.sonarLoopJob = CoroutineScope(Dispatchers.Default).launch {
                playSonar()
            }
        }

        // Exit vibration (only if enabled in settings AND vibrator exists)
        val vibrationEnabled =
            settingsSharedPref.getBoolean(SettingsActivity.KEY_VIBRATION, true)
        if (vibrationEnabled && vibrator != null && vibrator!!.hasVibrator()) {
            gameLogicViewModel.exitVibrationLoopJob =
                CoroutineScope(Dispatchers.Default).launch {
                    playExitVibration()
                }
        } else {
            if (vibrationEnabled && (vibrator == null || !vibrator!!.hasVibrator())) {
                Toast.makeText(
                    this,
                    "Vibration enabled, but no vibrator available",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    override fun onResume() {
        super.onResume()

        val manual = settingsSharedPref.getBoolean(SettingsActivity.KEY_MANUAL, true)

        // New / resumed round should be "alive" (actual paused state handled elsewhere)
        gameEnded = false

        // Reset orientation tracking & smoothing (not calibration baselines)
        lastPitchDeg = 0f
        lastRollDeg = 0f
        firstOrientationSample = true
        lastStep = 0f

        // Only need tilt sensors in inertial mode
        if (!manual) {
            tiltController.start()
            motionController.start()
        } else {
            tiltController.stop()
            motionController.stop()
        }

        // Reattach pause dialog onDismiss callback if needed (e.g., after rotation)
        val existingPauseDialog =
            supportFragmentManager.findFragmentByTag(Dialogs.PAUSE_DIALOG_TAG) as Dialogs?
        if (existingPauseDialog != null) {
            existingPauseDialog.dialogDismissedCallback = object : Dialogs.DialogDismissListener {
                override fun onDialogDismissed() {
                    gameLogicViewModel.isPaused = false
                }
            }
        }
        else {
            // If there is no open pause dialog, resume game
            gameLogicViewModel.isPaused = false
        }
    }

    override fun onPause() {
        super.onPause()
        // Paused = game not running, also stop sensors
        gameLogicViewModel.isPaused = true
        motionController.stop()
        tiltController.stop()
    }

    fun updateTimerText() {
        val newText = "Elapsed time: ${gameLogicViewModel.secondsElapsed} seconds"
        timerText.text = newText
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    suspend fun gameLoop() {
        while (true) {
            while (!gameLogicViewModel.isPaused && !gameEnded) {

                // Checks for if the player collided with the wall or goal
                when (playerViewModel.isCollide(mapViewModel)) {
                    1 -> gameOver()
                    2 -> playerVictory()
                }

                val manual = settingsSharedPref.getBoolean(SettingsActivity.KEY_MANUAL, true)

                if (manual) {
                    // ---- MANUAL / JOYSTICK MODE ----
                    // Map stays fixed; submarine rotates based on joystick direction.
                    gameView.gameRenderer.mapRotation = 0f

                    val displacementChange = 0.008f
                    when (playerDirection) {
                        Control.Direction.UP -> {
                            playerViewModel.playerObject!!.displace(
                                0.0f,
                                displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 180.0f
                        }
                        Control.Direction.DOWN -> {
                            playerViewModel.playerObject!!.displace(
                                0.0f,
                                -displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 0.0f
                        }
                        Control.Direction.LEFT -> {
                            playerViewModel.playerObject!!.displace(
                                -displacementChange,
                                0.0f,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 270.0f
                        }
                        Control.Direction.RIGHT -> {
                            playerViewModel.playerObject!!.displace(
                                displacementChange,
                                0.0f,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 90.0f
                        }
                        Control.Direction.DOWN_RIGHT -> {
                            playerViewModel.playerObject!!.displace(
                                displacementChange,
                                -displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 45.0f
                        }
                        Control.Direction.DOWN_LEFT -> {
                            playerViewModel.playerObject!!.displace(
                                -displacementChange,
                                -displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 315.0f
                        }
                        Control.Direction.UP_LEFT -> {
                            playerViewModel.playerObject!!.displace(
                                -displacementChange,
                                displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 225.0f
                        }
                        Control.Direction.UP_RIGHT -> {
                            playerViewModel.playerObject!!.displace(
                                displacementChange,
                                displacementChange,
                                0.0f
                            )
                            playerViewModel.playerObject!!.zRotate = 135.0f
                        }
                        Control.Direction.NONE -> { /* no movement */ }
                    }
                } else {
                    // ---- INERTIAL / TILT MODE ----

                    // Rotate MAP based on phone yaw (tilt controller)
                    val mapYaw = tiltController.getCameraYawDeg()
                    gameView.gameRenderer.mapRotation = mapYaw

                    // Keep sub nose-up at the top of the screen
                    playerViewModel.playerObject?.zRotate = 180f - mapYaw

                    // Use slide motion to move the sub
                    if (!gameEnded) {
                        updateMovementFromAcceleration()
                    }
                }

                // Draws approximately 60 frames per second
                gameView.requestRender()
                delay(15)
            }
            // Tiny delay so the outer loop doesn't spin if paused/ended
            delay(20)
        }
    }

    // Use sliding acceleration (MotionController) to move the submarine forward/back
    private fun updateMovementFromAcceleration() {
        val now = System.currentTimeMillis()

        // 1) During a yaw snap, DO NOT MOVE (no translation during rotation)
        if (now < turnLockUntil) {
            return
        }

        // 2) Read current orientation (we only care that phone stays flat & not actively tilting)
        val (pitchDeg, rollDeg) = tiltController.getPitchRollDeg()
        val mapYawDeg = tiltController.getCameraYawDeg()

        // First sample: just initialise history and bail out (no movement yet)
        if (firstOrientationSample) {
            lastPitchDeg = pitchDeg
            lastRollDeg = rollDeg
            firstOrientationSample = false
            lastStep = 0f
            return
        }

        // 3) If the phone is not roughly flat, don't move at all
        if (abs(pitchDeg) > 20f || abs(rollDeg) > 20f) {
            lastPitchDeg = pitchDeg
            lastRollDeg = rollDeg
            return
        }

        // 4) If orientation is actively changing (you're tilting), don't move the sub.
        val pitchDelta = abs(pitchDeg - lastPitchDeg)
        val rollDelta = abs(rollDeg - lastRollDeg)

        if (pitchDelta > tiltDeltaThreshold || rollDelta > tiltDeltaThreshold) {
            lastPitchDeg = pitchDeg
            lastRollDeg = rollDeg
            return
        }

        // 5) Sliding the phone forward/back on the table should move the sub forward/back.
        val aForward = motionController.ay

        // Ignore tiny acceleration (noise) – no push => no movement
        if (abs(aForward) < accelDeadband) {
            lastPitchDeg = pitchDeg
            lastRollDeg = rollDeg
            lastStep = 0f
            return
        }

        // Direction and magnitude directly from current acceleration
        // Pushing phone FORWARD moves the sub FORWARD.
        val sign = if (aForward > 0f) 1f else -1f
        val mag = (abs(aForward) - accelDeadband).coerceAtLeast(0f)

        // Raw step size in "forward" direction.
        var step = sign * (baseStep + mag * stepGain)

        // Clamp step so a big shove doesn't fling the sub across the maze in one frame
        if (step > maxStep) step = maxStep
        if (step < -maxStep) step = -maxStep

        // Smooth the step to avoid jumps
        step = lastStep + stepSmoothFactor * (step - lastStep)

        // Ignore really tiny smoothed steps
        if (abs(step) < minStep) {
            lastPitchDeg = pitchDeg
            lastRollDeg = rollDeg
            lastStep = 0f
            return
        }

        // Map rotation angle in radians
        val theta = Math.toRadians(mapYawDeg.toDouble())

        // World-space direction that visually appears as "up" on screen,
        // aligned with how we rotate the camera in GameRenderer.
        val dx = (step * sin(theta)).toFloat()
        val dy = (step * cos(theta)).toFloat()

        playerViewModel.playerObject!!.displace(dx, dy, 0f)

        // Update orientation history after moving
        lastPitchDeg = pitchDeg
        lastRollDeg = rollDeg
        lastStep = step
    }

    // ----- Sound & vibration -----

    @RequiresApi(Build.VERSION_CODES.Q)
    suspend fun playSonar() {
        // Keep this coroutine alive until the game fully ends
        while (!gameEnded) {
            if (!gameLogicViewModel.isPaused) {
                // Calculate time until next sonar ping based on distance to nearest wall
                var delayTime = playerViewModel.distToNearestMazeWall(mapViewModel)
                delayTime = delayTime * delayTime
                delayTime =
                    2000 * (3.5f * delayTime) + (mediaPlayer?.duration ?: 0) + 100

                // Wait and then ping (if still running)
                delay(delayTime.toLong())
                if (!gameLogicViewModel.isPaused && !gameEnded) {
                    mediaPlayer?.start()
                }
            } else {
                // Game paused – just sleep briefly and re-check
                delay(100L)
            }
        }
    }

    // Hot–cold vibration:
    //  - Always vibrates with a light, slower pulse ("cold").
    //  - When within NEAR_EXIT_DIST of the exit, pulses get stronger and faster ("hot").
    @RequiresApi(Build.VERSION_CODES.Q)
    suspend fun playExitVibration() {
        // Larger radius so on hard maps you feel "hot" sooner around the exit region.
        val NEAR_EXIT_DIST = 4.5f   // was 2.0f; increased for hard-map fairness

        // Baseline (cold) pulse
        val BASE_AMPLITUDE = 40        // out of 255
        val BASE_DURATION_MS = 40L
        val BASE_DELAY_MS = 280L

        // Hot-zone ranges
        val HOT_MIN_AMPLITUDE = 80     // when just entering the hot zone
        val HOT_MAX_AMPLITUDE = 255    // right on top of exit
        val HOT_MIN_DURATION_MS = 60L
        val HOT_MAX_DURATION_MS = 180L
        val HOT_MIN_DELAY_MS = 60L     // very close = frequent pulses
        val HOT_MAX_DELAY_MS = 250L    // edge of hot zone

        while (!gameEnded) {
            if (gameLogicViewModel.isPaused) {
                // Game paused – skip vibration and wait a bit
                delay(150L)
                continue
            }

            // Distance from player to exit.
            val distToExit = playerViewModel.distToPointMazeEnd(mapViewModel)

            // Default to baseline "cold" values
            var amplitude = BASE_AMPLITUDE
            var durationMs = BASE_DURATION_MS
            var delayAfterMs = BASE_DELAY_MS

            if (!distToExit.isNaN() && distToExit <= NEAR_EXIT_DIST) {
                // Inside the hot zone: scale intensity by proximity.
                val proximity = (1f - (distToExit / NEAR_EXIT_DIST))
                    .coerceIn(0f, 1f)

                amplitude = (HOT_MIN_AMPLITUDE +
                        proximity * (HOT_MAX_AMPLITUDE - HOT_MIN_AMPLITUDE))
                    .toInt()
                    .coerceIn(1, 255)

                durationMs = (HOT_MIN_DURATION_MS +
                        proximity * (HOT_MAX_DURATION_MS - HOT_MIN_DURATION_MS)).toLong()

                delayAfterMs = (HOT_MAX_DELAY_MS -
                        proximity * (HOT_MAX_DELAY_MS - HOT_MIN_DELAY_MS)).toLong()
            }

            if (vibrator != null && vibrator!!.hasVibrator()) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    val effect = VibrationEffect.createOneShot(durationMs, amplitude)
                    vibrator!!.vibrate(effect)
                }
            } else {
                // No vibrator – nothing to do, just back off a bit
                delay(400L)
                continue
            }

            // Wait before the next pulse (always something, so it's "consistent")
            delay(delayAfterMs)
        }

        runOnUiThread {
            Toast.makeText(this, "Exit vibration loop stopped.", Toast.LENGTH_SHORT).show()
        }
    }

    fun playerVictory() {
        if (gameEnded) return
        gameCleanup()

        val dialog = Dialogs()
        val bundle = Bundle()
        bundle.putInt(Dialogs.DIALOG_ARG_KEY, Dialogs.VICTORY)
        bundle.putInt(Dialogs.TOTAL_TIME_ARG_KEY, gameLogicViewModel.secondsElapsed)
        bundle.putInt(Dialogs.DIFFICULTY_ARG_KEY, difficulty)
        dialog.arguments = bundle

        dialog.show(supportFragmentManager, Dialogs.VICTORY_DIALOG_TAG)
    }

    fun gameOver() {
        if (gameEnded) return
        gameCleanup()

        val dialog = Dialogs()
        val bundle = Bundle()
        bundle.putInt(Dialogs.DIALOG_ARG_KEY, Dialogs.GAME_OVER)
        bundle.putInt(Dialogs.DIFFICULTY_ARG_KEY, difficulty)
        dialog.arguments = bundle

        dialog.show(supportFragmentManager, Dialogs.GAME_OVER_DIALOG_TAG)
    }

    fun gameCleanup() {
        gameEnded = true
        gameLogicViewModel.isPaused = true
        gameLogicViewModel.gameStarted = false

        gameLogicViewModel.gameLoopJob?.cancel()

        mediaPlayer?.stop()
        gameLogicViewModel.sonarLoopJob?.cancel()
        gameLogicViewModel.exitVibrationLoopJob?.cancel()
        vibrator?.cancel()
    }

    override fun onDestroy() {
        super.onDestroy()
        gameEnded = true
        gameLogicViewModel.isPaused = true

        timerHandler.removeCallbacks(timerRunnable)
        gameLogicViewModel.gameLoopJob?.cancel()

        mediaPlayer?.stop()
        gameLogicViewModel.sonarLoopJob?.cancel()
        gameLogicViewModel.exitVibrationLoopJob?.cancel()
        vibrator?.cancel()

        if (::motionController.isInitialized) motionController.stop()
        if (::tiltController.isInitialized) tiltController.stop()
    }

    // Helper function for creating player and map data view models
    private fun getDrawableUri(name: String): Uri {
        return ("android.resource://com.example.cmpt362_group30_maze_game/drawable/" + name).toUri()
    }

    // Helper function for resolving the device vibrator
    private fun resolveVibrator(): Vibrator? {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            val vm =
                getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
            Log.d("debug", "Using device vibrator (VibratorManager)")
            vm.defaultVibrator
        } else {
            @Suppress("DEPRECATION")
            (getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).also {
                Log.d("debug", "Using device vibrator (legacy)")
            }
        }
    }
}
