package club.clubk.ktag.apps.sentry

import android.app.Application
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.media.ToneGenerator
import kotlin.math.sin
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.graphics.Bitmap
import android.graphics.Matrix
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.lifecycle.AndroidViewModel
import club.clubk.ktag.apps.core.UsbSerialManager
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.google.mediapipe.framework.image.BitmapImageBuilder
import com.google.mediapipe.tasks.core.BaseOptions
import com.google.mediapipe.tasks.vision.core.RunningMode
import com.google.mediapipe.tasks.vision.objectdetector.ObjectDetector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

data class DetectionResult(
    val left: Float,
    val top: Float,
    val right: Float,
    val bottom: Float,
    val confidence: Float
)

class SentryViewModel(application: Application) : AndroidViewModel(application) {

    private val mainHandler = Handler(Looper.getMainLooper())

    private val _connectionState = MutableStateFlow(UsbSerialManager.ConnectionState.DISCONNECTED)
    val connectionState: StateFlow<UsbSerialManager.ConnectionState> = _connectionState.asStateFlow()

    private val _availableDevices = MutableStateFlow<List<UsbSerialDriver>>(emptyList())
    val availableDevices: StateFlow<List<UsbSerialDriver>> = _availableDevices.asStateFlow()

    private val _detectionResult = MutableStateFlow<DetectionResult?>(null)
    val detectionResult: StateFlow<DetectionResult?> = _detectionResult.asStateFlow()

    private val _lastTriggerMs = MutableStateFlow(0L)
    val lastTriggerMs: StateFlow<Long> = _lastTriggerMs.asStateFlow()

    private val _config = MutableStateFlow(SentryConfig.fromPreferences(application))
    val config: StateFlow<SentryConfig> = _config.asStateFlow()

    private val _statusMessage = MutableStateFlow<String?>(null)
    val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()

    private val usbSerialManager = UsbSerialManager(
        application,
        object : UsbSerialManager.Listener {
            override fun onDataReceived(data: ByteArray) {
                // Sentry doesn't process incoming serial data
            }

            override fun onConnectionStateChanged(state: UsbSerialManager.ConnectionState) {
                mainHandler.post { _connectionState.value = state }
            }

            override fun onError(message: String) {
                mainHandler.post { _statusMessage.value = message }
            }

            override fun onDevicesChanged(devices: List<UsbSerialDriver>) {
                mainHandler.post { _availableDevices.value = devices }
            }

            override fun onAutoConnecting(driver: UsbSerialDriver) {
                // Auto-connect is handled implicitly
            }
        },
        actionUsbPermission = "club.clubk.ktag.apps.sentry.USB_PERMISSION"
    )

    private val toneGenerator = ToneGenerator(AudioManager.STREAM_MUSIC, ToneGenerator.MAX_VOLUME)
    @Volatile private var isDetectionTonePlaying = false

    // Model file must be placed in src/main/assets/efficientdet_lite0.tflite.
    // Download from: https://tfhub.dev/tensorflow/lite-model/efficientdet/lite0/detection/metadata/1
    private val objectDetector: ObjectDetector by lazy {
        val baseOptions = BaseOptions.builder()
            .setModelAssetPath("efficientdet_lite0.tflite")
            .build()
        val options = ObjectDetector.ObjectDetectorOptions.builder()
            .setBaseOptions(baseOptions)
            .setRunningMode(RunningMode.IMAGE)
            .setMaxResults(5)
            .setScoreThreshold(0.5f)
            .build()
        ObjectDetector.createFromOptions(getApplication(), options)
    }

    fun registerUsb() {
        usbSerialManager.register()
    }

    fun unregisterUsb() {
        usbSerialManager.unregister()
    }

    fun connect(driver: UsbSerialDriver) {
        usbSerialManager.connect(driver)
    }

    fun disconnect() {
        usbSerialManager.disconnect()
    }

    fun reloadConfig() {
        _config.value = SentryConfig.fromPreferences(getApplication())
    }

    /**
     * Returns an ImageAnalysis.Analyzer to be attached to the CameraX ImageAnalysis use case.
     * The composable is responsible for creating and binding the use case.
     */
    fun buildAnalyzer(): ImageAnalysis.Analyzer = ImageAnalysis.Analyzer { imageProxy ->
        processImage(imageProxy)
    }

    private fun processImage(imageProxy: ImageProxy) {
        try {
            val bitmap = imageProxy.toBitmap()
            val rotationDegrees = imageProxy.imageInfo.rotationDegrees
            val rotatedBitmap = if (rotationDegrees != 0) {
                val matrix = Matrix().apply { postRotate(rotationDegrees.toFloat()) }
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
            } else {
                bitmap
            }

            val mpImage = BitmapImageBuilder(rotatedBitmap).build()
            val results = objectDetector.detect(mpImage)

            val personDetection = results.detections()
                .filter { it.categories().any { cat -> cat.categoryName() == "person" } }
                .maxByOrNull { it.categories().first { cat -> cat.categoryName() == "person" }.score() }

            if (personDetection == null) {
                mainHandler.post {
                    _detectionResult.value = null
                    stopDetectionTone()
                }
                return
            }

            val box = personDetection.boundingBox()
            val w = rotatedBitmap.width.toFloat()
            val h = rotatedBitmap.height.toFloat()
            val result = DetectionResult(
                left = (box.left / w).coerceIn(0f, 1f),
                top = (box.top / h).coerceIn(0f, 1f),
                right = (box.right / w).coerceIn(0f, 1f),
                bottom = (box.bottom / h).coerceIn(0f, 1f),
                confidence = personDetection.categories().first { it.categoryName() == "person" }.score()
            )
            val intersectsCrosshair = result.left <= 0.5f && 0.5f <= result.right &&
                result.top <= 0.5f && 0.5f <= result.bottom
            mainHandler.post {
                _detectionResult.value = result
                if (_config.value.playDetectionTone) startDetectionTone() else stopDetectionTone()
                if (intersectsCrosshair) {
                    maybeTrigger()
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "Detection failed", e)
            mainHandler.post {
                _detectionResult.value = null
                stopDetectionTone()
            }
        } finally {
            imageProxy.close()
        }
    }

    private fun maybeTrigger() {
        val cfg = _config.value
        val now = System.currentTimeMillis()
        if (now - _lastTriggerMs.value >= cfg.cooldownMs) {
            _lastTriggerMs.value = now
            val trigger_press_command = "KEvent 7 0\n"
            usbSerialManager.write(trigger_press_command.toByteArray(Charsets.UTF_8))
            Log.i(TAG, "Triggered: $trigger_press_command")
            playTriggerSound()
            val trigger_release_command = "KEvent 8 0\n"
            usbSerialManager.write(trigger_release_command.toByteArray(Charsets.UTF_8))
        }
    }

    private fun startDetectionTone() {
        if (isDetectionTonePlaying) return
        isDetectionTonePlaying = true

        val sampleRate = 44100
        val frequency = 880.0 // A5 — one octave above the standard 440 Hz dial tone
        val bufferSize = AudioTrack.getMinBufferSize(
            sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT
        )
        val track = AudioTrack.Builder()
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build()
            )
            .setAudioFormat(
                AudioFormat.Builder()
                    .setSampleRate(sampleRate)
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build()
            )
            .setBufferSizeInBytes(bufferSize)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build()
        track.play()

        Thread {
            val buffer = ShortArray(bufferSize / 2)
            var phase = 0.0
            val phaseIncrement = 2.0 * Math.PI * frequency / sampleRate
            try {
                while (isDetectionTonePlaying) {
                    for (i in buffer.indices) {
                        buffer[i] = (Short.MAX_VALUE * sin(phase)).toInt().toShort()
                        phase += phaseIncrement
                        if (phase >= 2.0 * Math.PI) phase -= 2.0 * Math.PI
                    }
                    if (track.write(buffer, 0, buffer.size) < 0) break
                }
            } catch (e: Exception) {
                Log.w(TAG, "Detection tone thread error", e)
            } finally {
                track.stop()
                track.release()
            }
        }.start()
    }

    private fun stopDetectionTone() {
        isDetectionTonePlaying = false
        // The tone thread sees the flag, exits its loop, and releases the AudioTrack in its finally block.
    }

    private fun playTriggerSound() {
        try {
            toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 200)
        } catch (e: Exception) {
            Log.w(TAG, "Could not play trigger sound", e)
        }
    }

    override fun onCleared() {
        super.onCleared()
        stopDetectionTone()
        objectDetector.close()
        toneGenerator.release()
    }

    companion object {
        private const val TAG = "SentryViewModel"
    }
}
