// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle

import android.app.Application
import space.rcmd.android.sizzle.audio.AudioEngine
import space.rcmd.android.sizzle.input.BindingStore
import space.rcmd.android.sizzle.input.GamepadInput
import space.rcmd.android.sizzle.input.InputRouter
import space.rcmd.android.sizzle.input.KeyboardInput
import space.rcmd.android.sizzle.input.MidiInput
import space.rcmd.android.sizzle.io.PresetLibrary
import space.rcmd.android.sizzle.io.ProjectStore
import space.rcmd.android.sizzle.io.SettingsStore
import space.rcmd.android.sizzle.io.SongLibrary
import space.rcmd.android.sizzle.io.ThemeLibrary
import space.rcmd.android.sizzle.model.Project
import java.io.File
import kotlinx.coroutines.runBlocking

/**
 * The Application object is our (very small) dependency container. Instead of a
 * DI framework — overkill for a learning-friendly codebase — the handful of
 * app-wide singletons live here and are reached via `application as SizzleApp`.
 *
 * These objects outlive any single screen so playback and input keep working
 * across rotation and tab switches.
 */
class SizzleApp : Application() {

    /** The single in-memory song. Everything edits this one object. */
    val project: Project by lazy { Project() }

    /** Neutral input bus shared by touch / keyboard / gamepad / MIDI. */
    val inputRouter: InputRouter by lazy { InputRouter() }

    /** The real-time sound engine (started/stopped by the PlaybackService). */
    val audioEngine: AudioEngine by lazy { AudioEngine() }

    // Input source handlers are singletons so their (re)bindable maps are shared
    // between MainActivity (which feeds them hardware events) and the Settings
    // screen (which edits their bindings via MIDI-learn / gamepad rebind).
    val keyboardInput: KeyboardInput by lazy { KeyboardInput(inputRouter) }
    val gamepadInput: GamepadInput by lazy { GamepadInput(inputRouter) }
    val midiInput: MidiInput by lazy { MidiInput(this, inputRouter) }

    /** Persists the input binding maps across restarts (DataStore-backed). */
    val bindingStore: BindingStore by lazy { BindingStore(this) }

    /** On-disk instrument/effect preset library in the app's scoped storage. */
    val presetLibrary: PresetLibrary by lazy { PresetLibrary(File(filesDir, "presets")) }

    /** On-disk `.sng` song library in the app's scoped storage. */
    val songLibrary: SongLibrary by lazy { SongLibrary(File(filesDir, "songs")) }

    /** On-disk user colour-theme library (`.szt`) in the app's scoped storage. */
    val themeLibrary: ThemeLibrary by lazy { ThemeLibrary(File(filesDir, "themes")) }

    /** Autosave of the whole project (crash recovery / resume). */
    val projectStore: ProjectStore by lazy { ProjectStore(File(filesDir, "autosave.sng")) }

    /** App-level settings persistence (theme, audio backend). */
    val settingsStore: SettingsStore by lazy { SettingsStore(this) }

    override fun onCreate() {
        super.onCreate()
        // Crash recovery: restore the last autosaved project before the engine or
        // UI read it, so a crash / process-death resumes where the user left off.
        projectStore.restore(project)
        // Reload the sample audio for any preset-backed Sampler/SoundFont slots —
        // the autosave keeps file references, not the decoded PCM, so this makes a
        // selected preset's samples sound again on a fresh start.
        presetLibrary.rehydrate(project)
        // Write the built-in factory presets (e.g. the Ambience reverb spaces) to the
        // on-disk library on first launch so they appear in the standard browser.
        presetLibrary.seedFactoryPresets()
        audioEngine.setProject(project)
        // Restore saved bindings before any hardware event can arrive. This is a
        // single small read; blocking briefly at cold start keeps the input maps
        // authoritative from the first keypress rather than racing an async load.
        runBlocking { bindingStore.loadInto(keyboardInput, gamepadInput, midiInput) }
        installCrashAutosave()
    }

    /** Flush an autosave of the current project on any uncaught exception, then let
     *  the default handler crash/report as usual — so state survives a crash. */
    private fun installCrashAutosave() {
        val previous = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
            runCatching { projectStore.save(project) }
            previous?.uncaughtException(thread, throwable)
        }
    }
}
