package de.ladkau.schwertundmagieonpebblecompanionapp

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import fi.iki.elonen.NanoHTTPD
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

private const val PORT = 8888
private const val TAG  = "SuM"

private data class DiskSlot(val episode: Int, val part: Char, val btnId: Int) {
    val fileName: String get() = "SCHWUM${episode}${part}.D64"
}

class MainActivity : AppCompatActivity() {

    private lateinit var tvStatus:  TextView
    private lateinit var tvTime:    TextView
    private lateinit var tvLog:     TextView
    private lateinit var scrollLog: ScrollView
    private lateinit var display:   C64DisplayView
    private lateinit var keyboard:  C64KeyboardView

    private val mainHandler = Handler(Looper.getMainLooper())
    private val timeFormat  = SimpleDateFormat("HH:mm:ss", Locale.getDefault())

    private var server:    CompanionServer? = null
    private val engine     = C64Engine()
    private var emuThread: Thread? = null
    @Volatile private var emuRunning = false
    private var soundEnabled = false

    // ---- disk slots ---------------------------------------------------------

    private val diskSlots = listOf(
        DiskSlot(1, 'A', R.id.btn_ep_1a), DiskSlot(1, 'B', R.id.btn_ep_1b),
        DiskSlot(2, 'A', R.id.btn_ep_2a), DiskSlot(2, 'B', R.id.btn_ep_2b),
        DiskSlot(3, 'A', R.id.btn_ep_3a), DiskSlot(3, 'B', R.id.btn_ep_3b),
        DiskSlot(4, 'A', R.id.btn_ep_4a), DiskSlot(4, 'B', R.id.btn_ep_4b),
    )
    private val episodeButtons = mutableMapOf<DiskSlot, Button>()

    // ---- Disk import (multi-select) -----------------------------------------

    private val diskPickerLauncher: ActivityResultLauncher<Intent> =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val uris = mutableListOf<Uri>()
                result.data?.clipData?.let { clip ->
                    for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri)
                } ?: result.data?.data?.let { uris.add(it) }

                var count = 0
                for (uri in uris) {
                    val name = displayNameForUri(uri) ?: continue
                    importFileToAssets(uri, name.uppercase())
                    appendLog("Imported: $name")
                    count++
                }
                if (count > 0) refreshDiskButtons()
                appendLog("Imported $count disk image(s)")
            }
        }

    // ---- lifecycle ----------------------------------------------------------

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tvStatus  = findViewById(R.id.tv_status)
        tvTime    = findViewById(R.id.tv_time)
        tvLog     = findViewById(R.id.tv_log)
        scrollLog = findViewById(R.id.scroll_log)
        display   = findViewById(R.id.c64_display)
        keyboard  = findViewById(R.id.c64_keyboard)

        findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
            launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
        }

        val btnSound = findViewById<Button>(R.id.btn_sound)
        btnSound.setOnClickListener {
            soundEnabled = !soundEnabled
            engine.setSoundEnabled(soundEnabled)
            btnSound.text = if (soundEnabled) "Sound: ON" else "Sound: OFF"
        }

        for (slot in diskSlots) {
            val btn = findViewById<Button>(slot.btnId)
            episodeButtons[slot] = btn
            btn.setOnClickListener { loadDiskSlot(slot) }
        }

        initEmulator()
        refreshDiskButtons()
        startHttpServer()
        wireKeyboard()
    }

    // ---- emulator -----------------------------------------------------------

    private fun initEmulator() {
        val assetDir = getExternalFilesDir(null) ?: filesDir
        assetDir.mkdirs()
        copyBundledRoms(assetDir)
        val romDir = assetDir.absolutePath
        val ok = engine.initEmulator(romDir)
        tvStatus.text = if (ok) "Emulator ready" else "Emulator init failed"
        Log.d(TAG, "initEmulator=$ok romDir=$romDir")

        emuRunning = true
        emuThread = Thread {
            while (emuRunning) {
                val t0 = System.currentTimeMillis()
                engine.runFrame()
                display.updateFrame(engine)
                val elapsed = System.currentTimeMillis() - t0
                if (elapsed < 20) Thread.sleep(20 - elapsed)
            }
        }.also { it.name = "emu-loop"; it.isDaemon = true; it.start() }
    }

    // ---- bundled ROM extraction ---------------------------------------------

    private val bundledRoms = listOf("kernal", "basic", "chargen", "1541")

    private fun copyBundledRoms(destDir: File) {
        for (name in bundledRoms) {
            val dest = File(destDir, name)
            if (dest.exists()) continue
            try {
                assets.open(name).use { input ->
                    dest.outputStream().use { output -> input.copyTo(output) }
                }
                Log.d(TAG, "Copied bundled ROM: $name")
            } catch (e: Exception) {
                Log.e(TAG, "Failed to copy bundled ROM $name", e)
            }
        }
    }

    // ---- virtual keyboard ---------------------------------------------------

    private fun wireKeyboard() {
        keyboard.onKeyEvent = { key, pressed ->
            engine.injectKey(key.code, pressed)
            if (pressed) {
                Log.d(TAG, "Key pressed: ${key.label}")
                mainHandler.post { appendLog("KEY: ${key.label}") }
            }
        }
    }

    // ---- HTTP server (watch bridge) -----------------------------------------

    private fun startHttpServer() {
        server = CompanionServer(PORT, timeFormat,
            onTimeFetched = { time ->
                mainHandler.post { tvTime.text = "Serving time: $time" }
            },
            onKey = { cmd ->
                Log.d(TAG, "Watch key: $cmd")
                mainHandler.post { appendLog("WATCH: $cmd") }
            }
        )
        try {
            server?.start()
            Log.d(TAG, "HTTP server started on port $PORT")
        } catch (e: Exception) {
            Log.e(TAG, "Failed to start server", e)
        }
    }

    // ---- disk helpers -------------------------------------------------------

    private fun diskFile(slot: DiskSlot): File? {
        val assetDir = getExternalFilesDir(null) ?: filesDir
        return assetDir.listFiles()?.firstOrNull { it.name.uppercase() == slot.fileName }
    }

    private fun refreshDiskButtons() {
        for (slot in diskSlots) {
            episodeButtons[slot]?.isEnabled = diskFile(slot) != null
        }
    }

    private fun loadDiskSlot(slot: DiskSlot) {
        val file = diskFile(slot) ?: run {
            appendLog("Disk not found: ${slot.fileName}")
            return
        }
        val ok = engine.loadDisk(file.absolutePath)
        appendLog(if (ok) "Loaded: ${slot.fileName}" else "Load failed: ${slot.fileName}")
    }

    // ---- file import helpers ------------------------------------------------

    private fun launchPicker(
        launcher: ActivityResultLauncher<Intent>,
        title: String,
        multiSelect: Boolean = false
    ) {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
            if (multiSelect) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        }
        launcher.launch(Intent.createChooser(intent, title))
    }

    private fun importFileToAssets(uri: Uri, fileName: String): File {
        val assetDir = getExternalFilesDir(null) ?: filesDir
        val dest = File(assetDir, fileName)
        contentResolver.openInputStream(uri)?.use { input ->
            dest.outputStream().use { output -> input.copyTo(output) }
            Log.d(TAG, "Imported $uri → ${dest.absolutePath}")
        } ?: appendLog("Could not open: $uri")
        return dest
    }

    private fun displayNameForUri(uri: Uri): String? =
        contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
            ?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }

    // ---- helpers ------------------------------------------------------------

    private fun appendLog(entry: String) {
        val ts = timeFormat.format(Date())
        tvLog.text = "[$ts] $entry\n${tvLog.text}"
        scrollLog.post { scrollLog.scrollTo(0, 0) }
    }

    override fun onDestroy() {
        emuRunning = false
        emuThread?.join(500)
        server?.stop()
        super.onDestroy()
    }
}

// ---------------------------------------------------------------------------

class CompanionServer(
    port: Int,
    private val timeFormat: SimpleDateFormat,
    private val onTimeFetched: (String) -> Unit,
    private val onKey: (String) -> Unit
) : NanoHTTPD(port) {

    override fun serve(session: IHTTPSession): Response {
        val response = when (session.uri) {
            "/time" -> {
                val time = timeFormat.format(Date())
                onTimeFetched(time)
                newFixedLengthResponse(
                    Response.Status.OK, "application/json", """{"time":"$time"}"""
                )
            }
            "/key" -> {
                val cmd = session.parameters["cmd"]?.firstOrNull().orEmpty()
                if (cmd.isNotEmpty()) onKey(cmd)
                newFixedLengthResponse(Response.Status.OK, "text/plain", "ok")
            }
            else -> newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "not found")
        }
        response.addHeader("Access-Control-Allow-Origin", "*")
        return response
    }
}
